diff --git a/internal/player/unified_ffmpeg_player.go b/internal/player/unified_ffmpeg_player.go index 7f2f339..08ffa8f 100644 --- a/internal/player/unified_ffmpeg_player.go +++ b/internal/player/unified_ffmpeg_player.go @@ -122,7 +122,7 @@ func (p *UnifiedPlayer) Load(path string, offset time.Duration) error { if strings.Contains(path, "bbb_sunflower_2160p_60fps_normal.mp4") { logging.Debug(logging.CatPlayer, "Loading test video: Big Buck Bunny (%s)", path) } - + p.currentPath = path p.state = StateLoading @@ -135,27 +135,25 @@ func (p *UnifiedPlayer) Load(path string, offset time.Duration) error { // Create pipes for FFmpeg communication p.videoPipeReader, p.videoPipeWriter = io.Pipe() - p.audioPipeReader, p.audioPipeWriter = io.Pipe() + if !p.previewMode { + p.audioPipeReader, p.audioPipeWriter = io.Pipe() + } - // Build FFmpeg command with unified A/V output + // Build FFmpeg command - focus on video first args := []string{ "-hide_banner", "-loglevel", "error", "-ss", fmt.Sprintf("%.3f", offset.Seconds()), "-i", path, - // Video stream to pipe 4 "-map", "0:v:0", "-f", "rawvideo", "-pix_fmt", "rgb24", - "-r", "24", // We'll detect actual framerate - "pipe:4", - // Audio stream to pipe 5 - "-map", "0:a:0", - "-ac", "2", - "-ar", "48000", - "-f", "s16le", - "pipe:5", + "-r", "24", + "pipe:1", } + // Disable audio for now to get basic video working + args = append(args, "-an") + // Add hardware acceleration if available if p.config.HardwareAccel { if args = p.addHardwareAcceleration(args); args != nil { @@ -270,7 +268,8 @@ func (p *UnifiedPlayer) GetFrameImage() (*image.RGBA, error) { p.mu.Lock() defer p.mu.Unlock() - if p.state != StatePlaying || p.paused { + // Allow frame reading even when paused for UI updates + if p.state == StateStopped { return nil, nil } @@ -501,12 +500,25 @@ func (p *UnifiedPlayer) Play() error { return fmt.Errorf("no video loaded") } + if p.state == StateLoading { + // Still loading, wait + return fmt.Errorf("video still loading") + } + p.paused = false p.state = StatePlaying p.syncClock = time.Now() - + logging.Debug(logging.CatPlayer, "UnifiedPlayer: Play() called, state=%v", p.state) - + + // Start FFmpeg process if not already running + if p.cmd == nil || p.cmd.Process == nil { + if err := p.startVideoProcess(); err != nil { + p.state = StateStopped + return fmt.Errorf("failed to start video process: %w", err) + } + } + if p.stateCallback != nil { p.stateCallback(p.state) } @@ -608,49 +620,49 @@ func (p *UnifiedPlayer) startVideoProcess() error { // Start video frame reading goroutine if !p.previewMode { go func() { - rate := p.frameRate - if rate <= 0 { - rate = 24 - logging.Debug(logging.CatPlayer, "Frame rate unavailable; defaulting to %.0f fps", rate) - } - frameDuration := time.Second / time.Duration(rate) - frameTime := p.syncClock + rate := p.frameRate + if rate <= 0 { + rate = 24 + logging.Debug(logging.CatPlayer, "Frame rate unavailable; defaulting to %.0f fps", rate) + } + frameDuration := time.Second / time.Duration(rate) + frameTime := p.syncClock - for { - select { - case <-p.ctx.Done(): - logging.Debug(logging.CatPlayer, "Video processing goroutine stopped") - return + for { + select { + case <-p.ctx.Done(): + logging.Debug(logging.CatPlayer, "Video processing goroutine stopped") + return - default: - // Read frame from video pipe - frame, err := p.readVideoFrame() - if err != nil { - logging.Error(logging.CatPlayer, "Failed to read video frame: %v", err) - continue - } + default: + // Read frame from video pipe + frame, err := p.readVideoFrame() + if err != nil { + logging.Error(logging.CatPlayer, "Failed to read video frame: %v", err) + continue + } - if frame == nil { - continue - } + if frame == nil { + continue + } - // Update timing - p.currentTime = frameTime.Sub(p.syncClock) - frameTime = frameTime.Add(frameDuration) - p.syncClock = time.Now() + // Update timing + p.currentTime = frameTime.Sub(p.syncClock) + frameTime = frameTime.Add(frameDuration) + p.syncClock = time.Now() - // Notify callback - if p.frameCallback != nil { - p.frameCallback(p.GetCurrentFrame()) - } + // Notify callback + if p.frameCallback != nil { + p.frameCallback(p.GetCurrentFrame()) + } - // Sleep until next frame time - sleepTime := frameTime.Sub(time.Now()) - if sleepTime > 0 { - time.Sleep(sleepTime) + // Sleep until next frame time + sleepTime := frameTime.Sub(time.Now()) + if sleepTime > 0 { + time.Sleep(sleepTime) + } } } - } }() } @@ -686,10 +698,9 @@ func (p *UnifiedPlayer) readAudioStream() { // readVideoStream reads video frames from the video pipe func (p *UnifiedPlayer) readVideoFrame() (*image.RGBA, error) { - // Check if paused - skip reading frames while paused - if p.paused { - return nil, nil - } + // Allow frame reading when paused for UI updates + // but don't advance frame counter if paused + wasPaused := p.paused // Read RGB24 frame data from FFmpeg pipe frameSize := p.windowW * p.windowH * 3 // RGB24 = 3 bytes per pixel @@ -697,9 +708,16 @@ func (p *UnifiedPlayer) readVideoFrame() (*image.RGBA, error) { p.videoBuffer = make([]byte, frameSize) } - // Check for paused state before reading - if p.paused { - return nil, fmt.Errorf("player is paused") + // For non-blocking read when paused, use peek + if wasPaused { + // Return last known frame when paused (create placeholder if none) + img := p.frameBuffer.Get().(*image.RGBA) + if img.Rect.Dx() != p.windowW || img.Rect.Dy() != p.windowH { + img.Rect = image.Rect(0, 0, p.windowW, p.windowH) + img.Stride = p.windowW * 4 + img.Pix = make([]uint8, p.windowW*p.windowH*4) + } + return img, nil } // Read full frame - io.ReadFull ensures we get complete frame @@ -724,12 +742,13 @@ func (p *UnifiedPlayer) readVideoFrame() (*image.RGBA, error) { } utils.CopyRGBToRGBA(img.Pix, p.videoBuffer) - // Update frame counter - p.currentFrame++ - - // Notify time callback - if p.timeCallback != nil { - p.timeCallback(p.currentTime) + // Update frame counter only when not paused + if !wasPaused { + p.currentFrame++ + // Notify time callback + if p.timeCallback != nil { + p.timeCallback(p.currentTime) + } } return img, nil diff --git a/internal/player/unified_player_adapter.go b/internal/player/unified_player_adapter.go index 2afd54e..205c1c7 100644 --- a/internal/player/unified_player_adapter.go +++ b/internal/player/unified_player_adapter.go @@ -329,16 +329,16 @@ func (p *UnifiedPlayerAdapter) startFrameDisplayLoop() { return case <-ticker.C: p.mu.Lock() - if !p.paused && p.player != nil { - // Get frame from UnifiedPlayer - frame, err := p.player.GetFrameImage() - if err == nil && frame != nil { - fyne.CurrentApp().Driver().DoFromGoroutine(func() { - p.img.Image = frame - p.img.Refresh() - }, false) - } + // Always try to get frames, even when paused for UI updates + if p.player != nil { + frame, err := p.player.GetFrameImage() + if err == nil && frame != nil { + fyne.CurrentApp().Driver().DoFromGoroutine(func() { + p.img.Image = frame + p.img.Refresh() + }, false) } + } p.mu.Unlock() } } diff --git a/main.go b/main.go index 31f812c..9b02f8a 100644 --- a/main.go +++ b/main.go @@ -750,6 +750,7 @@ type convertConfig struct { AspectHandling string OutputAspect string AspectUserSet bool // Tracks if user explicitly set OutputAspect + ForceAspect bool // Force DAR/SAR metadata even when no aspect conversion TempDir string // Optional temp/cache directory override } @@ -818,6 +819,7 @@ func defaultConvertConfig() convertConfig { AspectHandling: "Auto", OutputAspect: "Source", AspectUserSet: false, + ForceAspect: true, TempDir: "", } } @@ -845,9 +847,14 @@ func loadPersistedConvertConfig() (convertConfig, error) { if err != nil { return cfg, err } + var raw map[string]json.RawMessage + _ = json.Unmarshal(data, &raw) if err := json.Unmarshal(data, &cfg); err != nil { return cfg, err } + if _, ok := raw["ForceAspect"]; !ok { + cfg.ForceAspect = true + } if cfg.OutputAspect == "" || strings.EqualFold(cfg.OutputAspect, "Source") { cfg.OutputAspect = "Source" cfg.AspectUserSet = false @@ -1236,6 +1243,14 @@ type appState struct { audioLeftPanel *fyne.Container audioSingleContent *fyne.Container audioBatchContent *fyne.Container + + // Application Preferences + defaultOutputDir string + defaultVideoCodec string // "libx264", "libx265", etc. + defaultAudioCodec string // "aac", "libmp3lame", etc. + hardwareAcceleration string // "auto", "none", "nvenc", "qsv", "vaapi" + uiTheme string // "Dark", "Light", "System" + autoPreview bool // Enable auto-preview functionality } type mergeClip struct { @@ -2398,8 +2413,10 @@ func (s *appState) addConvertToQueueForSource(src *videoSource, addToTop bool) e "coverArtPath": cfg.CoverArtPath, "aspectHandling": cfg.AspectHandling, "outputAspect": cfg.OutputAspect, + "forceAspect": cfg.ForceAspect, "sourceWidth": src.Width, "sourceHeight": src.Height, + "sampleAspectRatio": src.SampleAspectRatio, "sourceDuration": src.Duration, "sourceBitrate": src.Bitrate, "fieldOrder": src.FieldOrder, @@ -2528,8 +2545,10 @@ func (s *appState) addConvertToQueueForSourceWithOutputs(src *videoSource, used "coverArtPath": cfg.CoverArtPath, "aspectHandling": cfg.AspectHandling, "outputAspect": cfg.OutputAspect, + "forceAspect": cfg.ForceAspect, "sourceWidth": src.Width, "sourceHeight": src.Height, + "sampleAspectRatio": src.SampleAspectRatio, "sourceDuration": src.Duration, "sourceBitrate": src.Bitrate, "fieldOrder": src.FieldOrder, @@ -3414,8 +3433,10 @@ func (s *appState) batchAddToQueue(paths []string) { "coverArtPath": "", "aspectHandling": s.convert.AspectHandling, "outputAspect": s.convert.OutputAspect, + "forceAspect": s.convert.ForceAspect, "sourceWidth": src.Width, "sourceHeight": src.Height, + "sampleAspectRatio": src.SampleAspectRatio, "sourceBitrate": src.Bitrate, "sourceDuration": src.Duration, "fieldOrder": src.FieldOrder, @@ -4624,6 +4645,7 @@ func (s *appState) executeConvertJob(ctx context.Context, job *queue.Job, progre // Source metrics (used for filters and bitrate defaults) sourceWidth, _ := cfg["sourceWidth"].(int) sourceHeight, _ := cfg["sourceHeight"].(int) + sampleAspectRatio, _ := cfg["sampleAspectRatio"].(string) sourceBitrate := 0 if v, ok := cfg["sourceBitrate"].(float64); ok { sourceBitrate = int(v) @@ -4719,16 +4741,27 @@ func (s *appState) executeConvertJob(ctx context.Context, job *queue.Job, progre } } // Aspect ratio conversion - srcAspect := utils.AspectRatioFloat(sourceWidth, sourceHeight) + srcAspect := utils.DisplayAspectRatioFloat(sourceWidth, sourceHeight, sampleAspectRatio) outputAspect, _ := cfg["outputAspect"].(string) aspectHandling, _ := cfg["aspectHandling"].(string) + forceAspect := true + if v, ok := cfg["forceAspect"].(bool); ok { + forceAspect = v + } // Create temp source for aspect calculation - tempSrc := &videoSource{Width: sourceWidth, Height: sourceHeight} + tempSrc := &videoSource{Width: sourceWidth, Height: sourceHeight, SampleAspectRatio: sampleAspectRatio} targetAspect := resolveTargetAspect(outputAspect, tempSrc) if targetAspect > 0 && srcAspect > 0 && !utils.RatiosApproxEqual(targetAspect, srcAspect, 0.01) { vf = append(vf, aspectFilters(targetAspect, aspectHandling)...) } + if forceAspect && targetAspect > 0 { + if len(vf) == 0 { + vf = append(vf, fmt.Sprintf("setdar=%.6f", targetAspect), "setsar=1") + } else { + vf = appendAspectMetadata(vf, targetAspect) + } + } // Flip horizontal flipH, _ := cfg["flipHorizontal"].(bool) @@ -6207,7 +6240,8 @@ func buildFFmpegCommandFromJob(job *queue.Job) string { } // Aspect ratio handling (simplified) - if outputAspect, _ := cfg["outputAspect"].(string); outputAspect != "" && outputAspect != "Source" { + outputAspect, _ := cfg["outputAspect"].(string) + if outputAspect != "" && outputAspect != "Source" { aspectHandling, _ := cfg["aspectHandling"].(string) if aspectHandling == "letterbox" { vf = append(vf, fmt.Sprintf("pad=iw:iw*(%s/(sar*dar)):(ow-iw)/2:(oh-ih)/2", outputAspect)) @@ -6216,6 +6250,26 @@ func buildFFmpegCommandFromJob(job *queue.Job) string { } } + // Force aspect metadata when enabled + forceAspect := true + if v, ok := cfg["forceAspect"].(bool); ok { + forceAspect = v + } + if forceAspect { + sourceWidth, _ := cfg["sourceWidth"].(int) + sourceHeight, _ := cfg["sourceHeight"].(int) + sampleAspectRatio, _ := cfg["sampleAspectRatio"].(string) + tempSrc := &videoSource{Width: sourceWidth, Height: sourceHeight, SampleAspectRatio: sampleAspectRatio} + outputAspect, _ := cfg["outputAspect"].(string) + if targetAspect := resolveTargetAspect(outputAspect, tempSrc); targetAspect > 0 { + if len(vf) == 0 { + vf = append(vf, fmt.Sprintf("setdar=%.6f", targetAspect), "setsar=1") + } else { + vf = appendAspectMetadata(vf, targetAspect) + } + } + } + // Flipping if flipH, _ := cfg["flipHorizontal"].(bool); flipH { vf = append(vf, "hflip") @@ -6585,6 +6639,13 @@ func runGUI() { audioNormTruePeak: audioDefaults.NormTruePeak, audioOutputDir: audioDefaults.OutputDir, audioSelectedTracks: make(map[int]bool), + // Application Preferences defaults + defaultOutputDir: "", + defaultVideoCodec: "libx264", + defaultAudioCodec: "aac", + hardwareAcceleration: "auto", + uiTheme: "Dark", + autoPreview: true, } if cfg, err := loadPersistedConvertConfig(); err == nil { @@ -7916,6 +7977,26 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject { targetAspectSelect *widget.Select targetAspectSelectSimple *widget.Select ) + var forceAspectChecks []*widget.Check + syncForceAspect := func(checked bool) { + state.convert.ForceAspect = checked + for _, c := range forceAspectChecks { + if c.Checked != checked { + c.SetChecked(checked) + } + } + if buildCommandPreview != nil { + buildCommandPreview() + } + } + makeForceAspectCheck := func() *widget.Check { + check := widget.NewCheck("Force aspect metadata (DAR/SAR)", func(checked bool) { + syncForceAspect(checked) + }) + check.SetChecked(state.convert.ForceAspect) + forceAspectChecks = append(forceAspectChecks, check) + return check + } // Aspect select widget - uses state manager to eliminate sync flag targetAspectSelect = widget.NewSelect(aspectTargets, func(value string) { setAspect(value, true) @@ -9451,6 +9532,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject { widget.NewLabelWithStyle("Target Aspect Ratio", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), targetAspectSelectSimple, targetAspectHintContainer, + makeForceAspectCheck(), )) // Simple mode options - minimal controls, aspect locked to Source @@ -9530,6 +9612,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject { targetAspectSelect, targetAspectHintContainer, aspectBox, + makeForceAspectCheck(), )) autoCropSection := buildConvertBox("Auto-Crop", container.NewVBox( @@ -10136,8 +10219,10 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject { "coverArtPath": cfg.CoverArtPath, "aspectHandling": cfg.AspectHandling, "outputAspect": cfg.OutputAspect, + "forceAspect": cfg.ForceAspect, "sourceWidth": src.Width, "sourceHeight": src.Height, + "sampleAspectRatio": src.SampleAspectRatio, "sourceDuration": src.Duration, "fieldOrder": src.FieldOrder, } @@ -13057,8 +13142,12 @@ func (s *appState) startConvert(status *widget.Label, btn, cancelBtn *widget.But } targetAspect := resolveTargetAspect(cfg.OutputAspect, src) - if targetAspect > 0 && len(vf) > 0 { - vf = appendAspectMetadata(vf, targetAspect) + if cfg.ForceAspect && targetAspect > 0 { + if len(vf) == 0 { + vf = append(vf, fmt.Sprintf("setdar=%.6f", targetAspect), "setsar=1") + } else { + vf = appendAspectMetadata(vf, targetAspect) + } } // Flip horizontal @@ -13770,8 +13859,12 @@ func (s *appState) generateSnippet() { vf = append(vf, aspectFilters(targetAspect, s.convert.AspectHandling)...) } } - if targetAspect := resolveTargetAspect(s.convert.OutputAspect, src); targetAspect > 0 && len(vf) > 0 { - vf = appendAspectMetadata(vf, targetAspect) + if targetAspect := resolveTargetAspect(s.convert.OutputAspect, src); s.convert.ForceAspect && targetAspect > 0 { + if len(vf) == 0 { + vf = append(vf, fmt.Sprintf("setdar=%.6f", targetAspect), "setsar=1") + } else { + vf = appendAspectMetadata(vf, targetAspect) + } } // Frame rate conversion (only if explicitly set and different from source) diff --git a/vendor/fyne.io/fyne/v2/.gitignore b/vendor/fyne.io/fyne/v2/.gitignore new file mode 100644 index 0000000..a03d7f0 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/.gitignore @@ -0,0 +1,49 @@ +### Binaries and project specific files +cmd/fyne/fyne +cmd/fyne_demo/fyne_demo +cmd/fyne_settings/fyne_settings +cmd/hello/hello +fyne-cross +*.exe +*.apk +*.app +*.tar.xz +*.zip + +### Tests +**/testdata/failed + +### Go +# Output of the coverage tool +*.out + +### macOS +# General +.DS_Store + +# Thumbnails +._* + +### JetBrains +.idea + +### VSCode +.vscode + +### Vim +# Swap +[._]*.s[a-v][a-z] +[._]*.sw[a-p] +[._]s[a-v][a-z] +[._]sw[a-p] + +# Session +Session.vim + +# Temporary +.netrwhist +*~ +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ diff --git a/vendor/fyne.io/fyne/v2/.godocdown.import b/vendor/fyne.io/fyne/v2/.godocdown.import new file mode 100644 index 0000000..65b6416 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/.godocdown.import @@ -0,0 +1 @@ +fyne.io/fyne/v2 diff --git a/vendor/fyne.io/fyne/v2/AUTHORS b/vendor/fyne.io/fyne/v2/AUTHORS new file mode 100644 index 0000000..61109a4 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/AUTHORS @@ -0,0 +1,16 @@ +Andy Williams +Steve OConnor +Luca Corbo +Paul Hovey +Charles Corbett +Tilo Prütz +Stephen Houston +Storm Hess +Stuart Scott +Jacob Alzén +Charles A. Daniels +Pablo Fuentes +Changkun Ou +Cedric Bail +Drew Weymouth +Simon Dassow diff --git a/vendor/fyne.io/fyne/v2/CHANGELOG.md b/vendor/fyne.io/fyne/v2/CHANGELOG.md new file mode 100644 index 0000000..a4f74a4 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/CHANGELOG.md @@ -0,0 +1,1680 @@ +# Changelog + +This file lists the main changes with each version of the Fyne toolkit. +More detailed release notes can be found on the [releases page](https://github.com/fyne-io/fyne/releases). + +## 2.7.1 - 14 Nov 2025 + +### Fixed + +* Ensure tar files created in cli tool contain a root directory +* 2.7 regression in GL performance on Mac (#6010) +* GridWrap keyboard navigation does not change to next row at the end of previous row (#5994) +* Image border radius not always shown on image fill cover (#5980) +* Speed up file dialog rendering + + +## 2.7.0 - 16 Oct 2025 + +### Added + +* Canvas types: Arc, Polygon, Square (and Rectangle.Aspect) +* "Cover" image fill +* Fully rounded corner radius ("pill" rectangle) & per-corner +* Image corner radius +* New containers: Navigation & Clip +* New Embedded driver (for running on non-standard drvices) +* RowWrap layout +* Add Generics to List and Tree for data binding +* Support for IPv6 addresses in URIs +* Add storage.RemoveAll to recursively delete from a repository +* Added Portuguese, Russian & Chinese (Simplified) +* Support left-tap systray to show window (SetSystemTrayWindow) +* Support JSON theme with fallback +* Add RichText bullet start number +* Option to always show Entry validation + +### Changed + +* Massive performance increases on rendering +* optimisations galore in data handling, custom themes and TextGrid +* Smooth infinite progress by reversing animation not just looping +* Numerous memory leaks and potential race conditions addressed + +### Fixed + +* Theme Override container cannot be used to override layout paddings (#5019) +* Tree widget: First selected node stays highlighted after switching selection (#5796) +* Brave browser: Web app via fyne serve blinks and closes , works correctly in Chrome (and Edge) (#5705) +* layout.formLayout render canvas.Text as labels and values outside/off minSize area (#5163) +* Android: EGL_BAD_SURFACE fails to redraw app after OpenFile dialog closes (#3541) +* Tab and Shift +Tab not behaving as expected on folder dialog (#5974) + +### New Contributors + +Code in v2.7.0 contains work from the following first time contributors: + +* @kruitbosdotdev +* @cognusion +* @r3quie +* @redawl +* @generikvault +* @cwarden +* @Vinci10 +* @cpustejovsky +* @xfaris +* @rad756 +* @ystepanoff + + +## 2.6.3 - 21 August 2025 + +### Fixed + +* Resolve compile issue with Go 1.25.0 caused by golang.org/x/tools conflict + + +## 2.6.2 - 28 July 2025 + +### Changed + +* Added Czechoslovakia translations + +### Fixed + +* bounds check panic on undo after ctrl-deleting text on last line from MultiLineEntry (#5714) +* Entry OnChanged does not get triggered on undo and redo (#5710) +* SetText in TextGrid leaves trailing content (#5722) +* Desktop app with system tray hangs on app.Quit (#5724) +* CenterOnScreen Regression (#5733) +* TextGrid CursorLocationForPosition reports wrong location when scrolled (#5745) +* Language is always "en" on macOS (#5760) +* TextGrid is glitchy when calling SetText on a scrolled container. (#5762) +* When running for the second time in window.CenterOnScreen(), it will get stuck when running on the main thread (#5766) +* Text entry widget backspace deletes two characters on Android (#2774) +* Secondary windows is not refreshed correctly (#5782) +* Clicking a button rapidly causes the click animation to extend outside of the button (#5785) +* WASM cursor was ignored +* Corrected date format for Germany +* Hide() doesn't work at startup for widgets/containers (#5597) +* Android GBoard: first character typed into Entry is prefixed with "0" after focus (#5666) +* Use Scaled Monitor Size For Monitor Detection (#5802) +* Don't override user choice if a) xdg lookup fails or b) system updates (#5851) +* Entry with mobile.NumberKeyboard does not Type comma and separators (#5101) +* Padding value is ignored in ThemeOverride container +* Performance improvements in TextGrid and object positioning +* Improvements in WASM rendering performance + + +## 2.6.1 - 8 May 2025 + +### Changed + + * Added Russian translations + +### Fixed + + * Activity indicator is light and not visible when a light theme is active (#5661) + * Unsafe use of map in RichText on 2.6.0 (#5639) + * Image translucency causes blurriness on small icons (#5476) + * Infinite progress bar snapping and doesn't loop nicely (#5433) + * RichTextSegment SizeName is not SizeNameText by default (#5307) + * When there is an offline netdrive, the file dialog will freeze (#2411) + * Correctly reset image cache when Resource goes to nil + * Data race after migration to v2.6.0 (#5713) + +## 2.6.0 - 10 April 2025 + +### Added + + * Added [fyne.Do] and [fyne.DoAndWait] to call from goroutines. This makes it possible to eliminate race conditions. + * Add "Migrations" section to FyneApp.toml to mark migrations like `fyneDo = true` + * Add Calendar and DateEntry widgets + * Add a third state ([Check.Partial]) to the check widget (#3576) + * Add ability to select label text using new [Label.Selectable] + * Support for storage on web driver (#4634) + * test: Add RenderToMarkup and RenderObjectToMarkup (#5124) + * Add ability to choose a text size for label widget (#5561) + * Show soft keyboard on Web build with mobile device (#4394) + * APIs for testing dialogs (#2771) + * Add `ScrollToOffset` functions to collection widgets + * Add Prepend method to Accordion (#5418) + * Support Apple intel apps on M1/2 (using Rosetta) (#3971) + * Ability to turn off re-scaling when a window moves between monitors for Linux (#5164) + * Add functions to get text location for position (and vice-versa) with a TextGrid + * Add support for scrolling many lines in TextGrid + * Add `Append` function to TextGrid + * Add `Prepend` function to Accordion + * Support custom titles in file dialogs using `SetTitleText` + * Add utility methods to handle colouring of SVG images + * Add preference bind APIs for list (slice) types + * Added Greek, Ukrainian & Chinese (Simplified) translations + +### Changed + + * All callbacks from Fyne drivers and widgets now call on the same goroutine + * Shortcuts on menu items are now called before widget or canvas shortcuts (#2627) + * ActionItems in an Entry should now match the standard button size + * Tidy the fyne CLI and moved to tools repo (#4920) + * When scroll bar is expanded, clicking above or below the bar scrolls up or down (#4922) + * Add generics to data binding package + * File picker now ignores case (#5113) + * Consistent callback order for dialogs - data before OnClosed + * Improve drop-shadow to show light from top position + * load markdown images from origin when not a URL + * Debug now disabled by default for WASM builds + * Updated theme of inner window borders with circle style and more customisations + * Change Accordion.OpenAll when used with single-open accordion to open the first item instead of none + +### Fixed + + * Fixed all known race conditions + * Decouple clipboard from fyne.Window enhancement (#4418) + * Odd looking SelectEntry with long PlaceHolder (#4430) + * Crash when resizing mobile simulator window (#5397) + * Deadlock when creating widget too fast (#3203) + * Application crashes on .Resize() (#5206) + * Linux (ubuntu) menu shortcuts not working blocker (#5355) + * Slider snaps back to min-value on Android (#5430) + * SoftwareCanvas resize only works properly if it's the last step bug (#5548) + * Showing a disabled menu items with a non-SVG icon generates Fyne error bug (#5557) + * Trying to hide subsequently created popups in a goroutine results in Fyne runtime panic (#5564) + * Table passes negative index to onSelected function (#4917) + * storage.Move() fails to move directories (#5493) + * Tree and Table widgets refresh full content on any scroll or resize (#5456) + * Memory leak from widget renderers never being destroyed blocker (#4903) + * On MacOS SysTray menu does not show when clicked on an active space in second monitor (#5223) + * On MacOs systray tooltip does not show when full window app is active (#5282) + * Panic when opening and closing windows quickly bug (#3280) + * Goroutines showing same window at similar times can forget size races (#4535) + * Panic when confirming or dismissing file open dialog races (#3279) + * richImage may freeze the application in some cases. (#3510) + * Memory usage increases significantly per character in Entry (#2969) + * Submenus not working on mobile (#5398) + * ListWidget with data index out of bounds when modified bound data (#5227) + * After scrolling, first selection in a list jumps that item to the bottom of the container (#5605) + * Accordion could have incorrect layout with multiple items open + * Prevent tapping within a popup from dismissing it, even if non-modal (#5360) + * Resolved performance issues in text and custom theme handling + + +## 2.5.5 - 13 March 2025 + +### Fixed + +* Correct wasm build for Go 1.24 onwards + + +## 2.5.4 - 1 February 2025 + +### Changed + +* Added Tamil translation + +### Fixed + +* Checkbox not responding to click because it is too "large"? (#5331) +* Fix progressbar not showing label until first refresh +* FyneApp.toml causes BadLength error (#5272) +* Test suite: failure with locale/language different from 'en' (#5362) +* fix GridWrap crash when resizing to same size without creating renderer +* Submenus not working on mobile (#5398) +* Subtle scrolling bug in List when the last two items are of different size (#5281) +* File picker does not ignore case (#5113) +* Tab "OnSelected" doesn't appear to allow focussing tab content (#5454) +* Documentation fixes + + +## 2.5.3 - 15 December 2024 + +### Changed + +* Smoothly decelerate scroll on mobile +* Added Spanish translation + +### Fixed + +* Starting location can be wrong in file dialogs with custom repository (#5200) +* Improve how shortcut special keys for menu items are rendered on Windows and Linux (#5108) +* Blank page in Chrome for Android +* Mobile Entry: cursor arrows don't work (#5258) +* FileDialog does not handle relative file URIs well. (#5234) +* [Linux] Only change variant when color scheme changes +* [Linux] Window with list flickers in Wayland (#5133) +* Package command fails on OpenBSD (#5195) +* System theme fallback is not working with custom themes +* Translucency and images with Alpha channel (#1977) +* Performance regression when scrolling inside the file dialog (#4307) +* Empty but visible images consume high CPU on 2.4.x (#4345) +* Improved performance of text render caching +* nil pointer dereference in dialog.Resize() for color picker (#5236) +* Tiny files written in iOS may be empty +* Some SVG resources don't update appearance correctly with the theme (#3900) + + +## 2.5.2 - 15 October 2024 + +### Fixed + +* Sometimes fyne fails to setup dark mode on Windows (#4758) +* Memory leak in fontMetrics cache on desktop driver (#4010) +* Fix possible crash with badly formatted json translation +* Tree widget doesn't display higher elements until a user selects one (#5095, #4346) +* Update to the latest breaking API changes in go-text +* Fix wrapping / truncation of multi-font text runs (#4998) +* Test window title is not always set (#5116) +* Deadlock in Button CreateRenderer() (#5114) +* Fix possible crash in theme watching for windows +* Fix issue with Movies folder on darwin file dialog +* widget.Entry: validate when pasting from clipboard (#5058, #5028) +* Reduce contention in some widget locks +* Add Swedish translation +* Improvements to documentation +* Improved temp check for windows/msys +* Fix split in a theme override container + + +## 2.5.1 - 24 August 2024 + +### Fixed + + * Apps with translations in new languages would not be recognised (#5015) + * App ID can be ignored from metadata file with go build/run + * Typing Chinese characters in widget.Entry and perform undo/redo crashes the app (#5001) + * Assets would render Low-DPI before attaching to a canvas + * Single click in file dialog enters two directories (#5053) + * Light/Dark mode detection no longer works on Ubuntu with Fyne 2.5 (#5029) + * Scroll acceleration logic causes scrolling to "jump" suddenly on macOS (#5067) + * SetSystemTrayMenu doesn't work when run in goroutine (#5039) + * stack overflow when calling SetRowHeight in table UpdateCell callback (#5007) + * Resizing List causes visible items to refresh instead of resize (#4080) + * Child widget with Hoverable() interface keeps parent widget's Tapped() function from being called. (#3906) + * App Translation file is ignored / tries to load BR (#5015, #5040) + * Missing theme variant auto-switching (dark/light) for Windows (#4537) + * Get DoubleTapDelay from the OS if an API is available (#4448) + * Entry cursor is not visible with animations off (#4508) + * Redundant justify-content properties in CSS for centered-container class (#5045) + * Update go-text/render to avoid crashing when rendering certain bitmap fonts (#5042) + * Using container.NewThemeOverride leaks memory until window closing (#5000) + + +## 2.5.0 - 14 July 2024 + +### Added + + * Internationalisation support and translations (#605, #3249) + * Look up system fonts for glyphs that cannot be found embedded (#2572, #1579) + * Completed support for Wayland on Linux + * Completed support for the Web driver (*except file handling) + * Add support for XDG Desktop Portals when built with `-tags flatpak` on Linux + * Activity indicator widget + * InnerWindow and MultipleWindows containers + * ThemeOverride container for grouping items with a different theme + * Add `NativeWindow.RunNative` to use a native window handle (#4483) + * Ability to request display stays on - `SetDisableScreenBlanking` (#4534, #3007) + * Add Undo/Redo support for widget.Entry (#436) + * Add AppendMarkdown function to RichText + * Add option in List to hide separators (#3756) + * New CustomPaddedLayout for more fine-grained container padding + * Add SizeName property to Hyperlink widget + * Support Ctrl+[backspace/delete] to delete the word to the left or right of the cursor + * Add IntToFloat (and FloatToInt) in bindings Data binding (#4666) + * Add ScrollToOffset/GetScrollOffset APIs for collection widgets + * Add ColumnCount API to GridWrap widget + * Disable and Enable for Slider widget (#3551) + * Function `Remove` added to List bindings (#3100) + * Form layout can now be vertical (labels above field) or adaptive for mobile + * Add support for Bold, Italic and Underline for TextGrid (#1237) + * Add support for setting a custom resource as the font source for text (#808) + * New `test` functions `NewTempApp`, `NewTempWindow` and `TempWidgetRenderer` to free resources automatically + +### Changed + + * Fyne now depends on Go 1.19 at a minimum + * Round the corners of scroll bars with new theme value + * Improve contrast of text on highlight background colours + * Layout of iOS and Android apps will adapt when keyboard appears (#566, #2371) + * FyneApp.toml will now be loaded with `go build` (#4688) + * Text wrapping will now wrap in dialogs (#2602) + * System tray and tray menu icons on will now match theme on macOS (#4549) + * Triple clicking in an Entry widget now selects current line (#4328) + * About menu items will now override the macOS default About + * System tray no longer shows tooltips (until we have full support) + * Double tapping an item in a file dialog now selects and returns + * Widgets should now use `theme.ForWidget()` instead of `theme.Default()` or static helpers + +### Fixed + + * Kannada characters not rendering correctly (#2654) + * Notifications are not working on iOS (#4966) + * Incorrect scaling on Steam Deck screen Accessibility (#4896) + * Sometimes the last list row that should be visible doesn't show (#4909) + * RichText swallowing whitespace after Markdown links (#4613, #4340) + * Disabled app tabs can still be selected in popup menu (#4935) + * Don't show title when mouse hover on Systray menu (#4916) + * Trying to access a URL through canvas.NewImageFromURI() in a test results in a panic (#4863) + * Don't insert tab character in Entry when Shift+Tab typed + * Select Does Not Gain Focus When Tapped (#4767) + * binding.Untyped crashes when set to nil bug (#4807) + * Label and Slider not aligned in a FormItem (#4714) + * Windows: App Icon in Notification (#2592) + * Fix possible writing of empty preference data in some quit events + * Allow application to set default view (list/grid) of file dialog before showing it (#4595) + * Fix ScrollToOffset when viewport is larger than content size + * Incorrect row header width in widget.Table (#4370) + * Add missed truncation mode for hyperlink (#4335) + * AppTab does not display blue indicator line if you create it empty and then Append items to it later. + * Many optimisations in animation, draw speed, layout and widget size calculations + * DocTabItem update text doesn't update the underline select bar (graphic glitch) (#3106) + + +## 2.4.5 - 15 April 2024 + +### Fixed + +* iOS files write would fail when over 16KB +* storage.Delete not supported on Android/iOS (#2120) +* layout.formLayout do not handle canvas.Text well in second column (#4665) +* Fix building with ios17.4 (#4741) +* Support template icon for system tray menu icons +* Fix recognition of missing XDG user directories (#4650) +* FileDialog.SetOnClosed not always working (#4651) +* Upgrade GLFW for performance improvements and bug fixes +* Multiple select popups can crash during background operations (#4730) +* Controlling a negative slider with the left arrow key blocks after 8 steps (#4736) +* cmd/fyne: command "get" is broken with Go 1.22 (#4684) +* Race condition during system tray menu refresh (#4697) +* Fyne release on Linux does not set Metadata().Release to true (#4711) +* RichText leaks memory when replacing segments (#4723) + + +## 2.4.4 - 13 February 2024 + +### Fixed + +* Spaces could be appended to linux Exec command during packaging +* Secondary mobile windows would not size correctly when padded +* Setting Icon.Resource to nil will not clear rendering +* Dismiss iOS keyboard if "Done" is tapped +* Large speed improvement in Entry and GridWrap widgets +* tests fail with macOS Assertion failure in NSMenu (#4572) +* Fix image test failures on Apple Silicon +* High CPU use when showing CustomDialogs (#4574) +* Entry does not show the last (few) changes when updating a binding.String in a fast succession (#4082) +* Calling Entry.SetText and then Entry.Bind immediately will ignore the bound value (#4235) +* Changing theme while application is running doesn't change some parameters on some widgets (#4344) +* Check widget: hovering/tapping to the right of the label area should not activate widget (#4527) +* Calling entry.SetPlaceHolder inside of OnChanged callback freezes app (#4516) +* Hyperlink enhancement: underline and tappable area shouldn't be wider than the text label (#3528) +* Fix possible compile error from go-text/typesetting + + +## 2.4.3 - 23 December 2023 + +### Fixed + +* Fix OpenGL init for arm64 desktop devices +* System tray icon on Mac is showing the app ID (#4416) +* Failure with fyne release -os android/arm (#4174) +* Android GoBack with forcefully close the app even if the keyboard is up (#4257) +* *BSD systems using the wrong (and slow) window resize +* Optimisations to reduce memory allocations in List, GridWrap, driver and mime type handling +* Reduce calls to C and repeated size checks in painter and driver code + + +## 2.4.2 - 22 November 2023 + +### Fixed + +* Markdown only shows one horizontal rule (#4216) +* Spacer in HBox with hidden item will cause an additional trailing padding (#4259) +* Application crash when fast clicking the folders inside the file dialog (#4260) +* failed to initialise OpenGL (#437) +* App panic when clicking on a notification panel if there's a systray icon (#4385) +* Systray cannot be shown on Ubuntu (#3678, #4381) +* failed to initialise OpenGL on Windows dual-chip graphics cards (#437) +* Reduce memory allocations for each frame painted +* RichText may not refresh if segments manually replaced +* Correct URI.Extension() documentation +* Update for security fixes to x/sys and x/net +* Inconsistent rendering of Button widget (#4243) +* PasswordEntry initial text is not obscured (#4312) +* Pasting text in Entry does not update cursor position display (#4181) + + +## 2.4.1 - 9 October 2023 + +### Fixed + +* Left key on tree now collapses open branch +* Avoid memory leak in Android driver code +* Entry Field on Android in Landscape Mode Shows "0" (#4036) +* DocTabs Indicator remains visible after last tab is removed (#4220) +* Fix mobile simulation builds on OpenBSD +* Fix alignment of menu button on mobile +* Fix Compilation with Android NDK r26 +* Clicking table headers causes high CPU consumption (#4264) +* Frequent clicking on table may cause the program to not respond (#4210) +* Application stops responding when scrolling a table (#4263) +* Possible crash parsing malformed JSON color (#4270) +* NewFolderOpen: incomplete filenames (#2165) +* Resolve issue where storage.List could crash with short URI (#4271) +* TextTruncateEllipsis abnormally truncates strings with multi-byte UTF-8 characters (#4283) +* Last character doesn't appear in Select when there is a special character (#4293) +* Resolve random crash in DocTab (#3909) +* Selecting items from a list caused the keyboard to popup on Android (#4236) + + +## 2.4.0 - 1 September 2023 + +### Added + +* Rounded corners in rectangle (#1090) +* Support for emoji in text +* Layout debugging (with `-tags debug` build flag) (#3314) +* GridWrap collection widget +* Add table headers (#1658, #3594) +* Add mobile back button handling (#2910) +* Add option to disable UI animations (#1813) +* Text truncation ellipsis (#1659) +* Add support for binding tree data, include new `NewTreeWithData` +* Add support for OpenType fonts (#3245) +* Add `Window.SetOnDropped` to handle window-wide item drop on desktop +* Add lists to the types supported by preferences API +* Keyboard focus handling for all collection widgets +* Add APIs for refreshing individual items in collections (#3826) +* Tapping slider moves it to that position (#3650) +* Add `OnChangeEnded` callback to `Slider` (#3652) +* Added keyboard controls to `Slider` +* Add `NewWarningThemedResource` and `NewSuccessThemedResource` along with `NewColoredResource` (#4040) +* Custom hyperlink callback for rich text hyperlinks (#3335) +* Added `dialog.NewCustomWithoutButtons`, with a `SetButtons` method (#2127, #2782) +* Added `SetConfirmImportance` to `dialog.ConfirmDialog`. +* Added `FormDialog.Submit()` to close and submit the dialog if validation passes +* Rich Text image alignment (#3810) +* Bring back `theme.HyperlinkColor` (#3867) +* Added `Importance` field on `Label` to color the text +* Navigating in entry quickly with ctrl key (#2462) +* Support `.desktop` file metadata in `FyneApp.toml` for Linux and BSD +* Support mobile simulator on FreeBSD +* Add data binding boolean operators `Not`, `And` and `Or` +* Added `Entry.Append`, `Select.SetOptions`, `Check.SetText`, `FormDialog.Submit` +* Add `ShowPopUpAtRelativePosition` and `PopUp.ShowAtRelativePosition` +* Add desktop support to get key modifiers with `CurrentKeyModifiers` +* Add geometry helpers `NewSquareSize` and `NewSquareOffsetPos` +* Add `--pprof` option to fyne build commands to enable profiling +* Support compiling from Android (termux) + +### Changed + +* Go 1.17 or later is now required. +* Theme updated for rounded corners on buttons and input widgets +* `widget.ButtonImportance` is now `widget.Importance` +* The `Max` container and layout have been renamed `Stack` for clarity +* Refreshing an image will now happen in app-thread not render process, apps may wish to add async image load +* Icons for macOS bundles are now padded and rounded, disable with "-use-raw-icon" (#3752) +* Update Android target SDK to 33 for Play Store releases +* Focus handling for List/Tree/Table are now at the parent widget not child elements +* Accordion widget now fills available space - put it inside a `VBox` container for old behavior (#4126) +* Deprecated theme.FyneLogo() for later removal (#3296) +* Improve look of menu shortcuts (#2722) +* iOS and macOS packages now default to using "XCWildcard" provisioning profile +* Improving performance of lookup for theme data +* Improved application startup time + +### Fixed + +* Rendering performance enhancements +* `dialog.NewProgressInfinite` is deprecated, but dialog.NewCustom isn't equivalent +* Mouse cursor desync with Split handle when dragging (#3791) +* Minor graphic glitch with checkbox (#3792) +* binding.String===>Quick refresh *b.val will appear with new data reset by a call to OnChange (#3774) +* Fyne window becomes unresponsive when in background for a while (#2791) +* Hangs on repeated calls to `Select.SetSelected` in table. (#3684) +* `Select` has wrong height, padding and border (#4142) +* `widget.ImageSegment` can't be aligned. (#3505) +* Memory leak in font metrics cache (#4108) +* Don't panic when loading preferences with wrong type (#4039) +* Button with icon has wrong padding on right (#4124) +* Preferences don't all save when written in `CloseIntercept` (#3170) +* Text size does not update in Refresh for TextGrid +* DocTab selection underline not updated when deleting an Item (#3905) +* Single line Entry throws away selected text on submission (#4026) +* Significantly improve performance of large `TextGrid` and `Tree` widgets +* `List.ScrollToBottom` not scrolling to show the totality of the last Item (#3829) +* Setting `Position1` of canvas.Circle higher than `Position2` causes panic. (#3949) +* Enhance scroll wheel/touchpad scroll speed on desktop (#3492) +* Possible build issue on Windows with app metadata +* `Form` hint text has confusing padding to next widget (#4137) +* `Entry` Placeholder Style Only Applied On Click (#4035) +* Backspace and Delete key Do not Fire OnChanged Event (#4117) +* Fix `ProgressBar` text having the wrong color sometimes +* Window doesn't render when called for the first time from system tray and the last window was closed (#4163) +* Possible race condition in preference change listeners +* Various vulnerabilities resolved through updating dependencies +* Wrong background for color dialog (#4199) + + +## 2.3.5 - 6 June 2023 + +### Fixed + +* Panic with unsupported font (#3646) +* Temporary manifest file not closed after building on Windows +* Panic when using autogenerated quit menu and having unshown windows (#3870) +* Using `canvas.ImageScaleFastest` not working on arm64 (#3891) +* Disabled password Entry should also disable the ActionItem (#3908) +* Disabled RadioGroup does not display status (#3882) +* Negative TableCellID Row (#2857) +* Make sure we have sufficient space for the bar as well if content is tiny (#3898) +* Leak in image painter when replacing image.Image source regularly +* Links in Markdown/Rich Text lists breaks formatting (#2911) +* Crash when reducing window to taskbar with popup opened (#3877) +* RichText vertical scroll will truncate long content with horizontal lines (#3929) +* Custom metadata would not apply with `fyne release` command +* Horizontal CheckGroup overlap when having long text (#3005) +* Fix focused colour of coloured buttons (#3462) +* Menu separator not visible with light theme (#3814) + + +## 2.3.4 - 3 May 2023 + +### Fixed + +* Memory leak when switching theme (#3640) +* Systray MenuItem separators not rendered in macOS root menu (#3759) +* Systray leaks window handles on Windows (#3760) +* RadioGroup miscalculates label widths in horizontal mode (#3386) +* Start of selection in entry is shifted when moving too fast (#3804) +* Performance issue in widget.List (#3816) +* Moving canvas items (e.g. Images) does not cause canvas repaint (#2205) +* Minor graphic glitch with checkbox (#3792) +* VBox and HBox using heap memory that was not required +* Menu hover is slow on long menus + +## 2.3.3 - 24 March 2023 + +### Fixed + +* Linux, Windows and BSD builds could fail if gles was missing + + +## 2.3.2 - 20 March 2023 + +### Fixed + +* Fyne does not run perfectly on ARM-based MacOS platforms (#3639) * +* Panic on closing window in form submit on Мac M2 (#3397) * +* Wobbling slider effect for very small steps (#3648) +* Fix memory leak in test canvas refresh +* Optimise text texture memory by switching to single channel +* Packaging an android fyne app that uses tags can fail (#3641) +* NewAdaptiveGrid(0) blanks app window on start until first resize on Windows (#3669) +* Unnecessary refresh when sliding Split container +* Linux window resize refreshes all content +* Themed and unthemed svg resources can cache collide +* When packaging an ampersand in "Name" causes an error (#3195) +* Svg in ThemedResource without viewBox does not match theme (#3714) +* Missing menu icons in Windows system tray +* Systray Menu Separators don't respect the submenu placement (#3642) +* List row focus indicator disappears on scrolling (#3699) +* List row focus not reset when row widget is reused to display a new item (#3700) +* Avoid panic if accidental 5th nil is passed to Border container +* Mobile simulator not compiling on Apple M1/2 +* Cropped letters in certain cases with the new v2.3.0 theme (#3500) + +Many thanks indeed to [Dymium](https://dymium.io) for sponsoring an Apple +M2 device which allowed us to complete the marked (*) issues. + + +## 2.3.1 - 13 February 2023 + +### Changed + +* Pad app version to ensure Windows packages correctly (#3638) + +### Fixed + +* Custom shortcuts with fyne.KeyTab is not working (#3087) +* Running a systray app with root privileges resulted in panic (#3120) +* Markdown image with no title is not parsed (#3577) +* Systray app on macOS panic when started while machine sleeps (#3609) +* Runtime error with VNC on RaspbianOS (#2972) +* Hovered background in List widget isn't reset when scrolling reuses an existing list item (#3584) +* cmd/fyne package can't find FyneApp.toml when -src option has given (#3459) +* TextWrapWord will cause crash in RichText unverified (#3498) +* crash in widget.(*RichText).lineSizeToColumn (#3292) +* Crash in widget.(*Entry).SelectedText (#3290) +* Crash in widget.(*RichText).updateRowBounds.func1 (#3291) +* window is max size at all times (#3507) +* systray.Quit() is not called consistently when the app is closing (#3597) +* Software rendering would ignore scale for text +* crash when minimize a window which contains a stroked rectangle (#3552) +* Menu item would not appear disabled initially +* Wrong icon colour for danger and warning buttons +* Embedding Fyne apps in iFrame alignment issue +* Generated metadata can be in wrong directory +* Android RootURI may not exist when used for storage (#3207) + + +## 2.3.0 - 24 December 2022 + +### Added + +* Shiny new theme that was designed for us +* Improved text handling to support non-latin alphabets +* Add cloud storage and preference support +* Add menu icon and submenu support to system tray menus +* More button importance levels `ErrorImportance`, `WarningImportance` +* Support disabling of `AppTabs` and `DocTabs` items +* Add image support to rich text (#2366) +* Add CheckGroup.Remove (#3124) + +### Changed + +* The buttons on the default theme are no longer transparent, but we added more button importance types +* Expose a storage.ErrNotExists for non existing documents (#3083) +* Update `go-gl/glfw` to build against latest Glfw 3.3.8 +* List items in `widget.List` now implement the Focusable interface + +### Fixed + +* Displaying unicode or different language like Bengali doesn't work (#598) +* Cannot disable container.TabItem (#1904) +* Update Linux/XDG application theme to follow the FreeDesktop Dark Style Preference (#2657) +* Running `fyne package -os android` needs NDK 16/19c (#3066) +* Caret position lost when resizing a MultilineEntry (#3024) +* Fix possible crash in table resize (#3369) +* Memory usage surge when selecting/appending MultilineEntry text (#3426) +* Fyne bundle does not support appending when parameter is a directory +* Crash parsing invalid file URI (#3275) +* Systray apps on macOS can only be terminated via the systray menu quit button (#3395) +* Wayland Scaling support: sizes and distances are scaled wrong (#2850) +* Google play console minimum API level 31 (#3375) +* Data bound entry text replacing selection is ignored (#3340) +* Split Container does not respect item's Visible status (#3232) +* Android - Entry - OnSubmitted is not working (#3267) +* Can't set custom CGO_CFLAGS and CGO_LDFLAGS with "fyne package" on darwin (#3276) +* Text line not displayed in RichText (#3117) +* Segfault when adding items directly in form struct (#3153) +* Preferences RemoveValue does not save (#3229) +* Create new folder directly from FolderDialog (#3174) +* Slider drag handle is clipped off at minimum size (#2966) +* Entry text "flickering" while typing (#3461) +* Rendering of not changed canvas objects after an event (#3211) +* Form dialog not displaying hint text and validation errors (#2781) + + +## 2.2.4 - 9 November 2022 + +### Fixes + +* Iphone incorrect click coordinates in zoomed screen view (#3122) +* CachedFontFace seems to be causing crash (#3134) +* Fix possible compile error if "fyne build" is used without icon metadata +* Detect and use recent Android NDK toolchain +* Handle fyne package -release and fyne release properly for Android and iOS +* Fix issue with mobile simulation when systray used +* Fix incorrect size and position for radio focus indicator (#3137) + + +## 2.2.3 - 8 July 2022 + +### Fixed + +* Regression: Preferences are not parsed at program start (#3125) +* Wrappable RichText in a Split container causes crash (#3003, #2961) +* meta.Version is always 1.0.0 on android & ios (#3109) + + +## 2.2.2 - 30 June 2022 + +### Fixed + +* Windows missing version metadata when packaged (#3046) +* Fyne package would not build apps using old Fyne versions +* System tray icon may not be removed on app exit in Windows +* Emphasis in Markdown gives erroneous output in RichText (#2974) +* When last visible window is closed, hidden window is set visible (#3059) +* Do not close app when last window is closed but systrayMenu exists (#3092) +* Image with ImageFillOriginal not showing (#3102) + + +## 2.2.1 - 12 June 2022 + +### Fixed + +* Fix various race conditions and compatibility issues with System tray menus +* Resolve issue where macOS systray menu may not appear +* Updated yaml dependency to fix CVE-2022-28948 +* Tab buttons stop working after removing a tab (#3050) +* os.SetEnv("FYNE_FONT") doesn't work in v2.2.0 (#3056) + + +## 2.2.0 - 7 June 2022 + +### Added + +* Add SetIcon method on ToolbarAction (#2475) +* Access compiled app metadata using new `App.Metadata()` method +* Add support for System tray icon and menu (#283) +* Support for Android Application Bundle (.aab) (#2663) +* Initial support for OpenBSD and NetBSD +* Add keyboard shortcuts to menu (#682) +* Add technical preview of web driver and `fyne serve` command +* Added `iossimulator` build target (#1917) +* Allow dynamic themes via JSON templates (#211) +* Custom hyperlink callback (#2979) +* Add support for `.ico` file when compiling for windows (#2412) +* Add binding.NewStringWithFormat (#2890) +* Add Entry.SetMinRowsVisible +* Add Menu.Refresh() and MainMenu.Refresh() (#2853) +* Packages for Linux and BSD now support installing into the home directory +* Add `.RemoveAll()` to containers +* Add an AllString validator for chaining together string validators + +### Changed + +* Toolbar item constructors now return concrete types instead of ToolbarItem +* Low importance buttons no longer draw button color as a background +* ProgressBar widget height is now consistent with other widgets +* Include check in DocTabs menu to show current tab +* Don't call OnScrolled if offset did not change (#2646) +* Prefer ANDROID_NDK_HOME over the ANDROID_HOME ndk-bundle location (#2920) +* Support serialisation / deserialisation of the widget tree (#5) +* Better error reporting / handling when OpenGL is not available (#2689) +* Memory is now better reclaimed on Android when the OS requests it +* Notifications on Linux and BSD now show the application icon +* Change listeners for preferences no longer run when setting the same value +* The file dialog now shows extensions in the list view for better readability +* Many optimisations and widget performance enhancements +* Updated various dependencies to their latest versions + +### Fixed + +* SendNotification does not show app name on Windows (#1940) +* Copy-paste via keyboard don't work translated keyboard mappings on Windows (#1220) +* OnScrolled triggered when offset hasn't changed (#1868) +* Carriage Return (\r) is rendered as space (#2456) +* storage.List() returns list with nil elements for empty directories (#2858) +* Entry widget, position of cursor when clicking empty space (#2877) +* SelectEntry cause UI hang (#2925) +* Font cutoff with bold italics (#3001) +* Fyne error: Preferences load error (#2936, 3015) +* Scrolled List bad redraw when window is maximized (#3013) +* Linux and BSD packages not being installable if the name contained spaces + + +## 2.1.4 - 17 March 2022 + +### Fixed + +* SetTheme() is not fully effective for widget.Form (#2810) +* FolderOpenDialog SetDismissText is ineffective (#2830) +* window.Resize() does not work if SetFixedSize(true) is set after (#2819) +* Container.Remove() race causes crash (#2826, #2775, #2481) +* FixedSize Window improperly sized if contains image with ImageFillOriginal (#2800) + + +## 2.1.3 - 24 February 2022 + +### Fixed + +* The text on button can't be show correctly when use imported font (#2512) +* Fix issues with DocTabs scrolling (#2709) +* Fix possible crash for tapping extended Radio or Check item +* Resolve lookup of relative icons in FyneApp.toml +* Window not shown when SetFixedSize is used without Resize (#2784) +* Text and links in markdown can be rendered on top of each other (#2695) +* Incorrect cursor movement in a multiline entry with wrapping (#2698) + + +## 2.1.2 - 6 December 2021 + +### Fixed + +* Scrolling list bound to data programmatically causes nil pointer dereference (#2549) +* Rich text from markdown can get newlines wrong (#2589) +* Fix crash on 32bit operating systems (#2603) +* Compile failure on MacOS 10.12 Sierra (#2478) +* Don't focus widgets on mobile where keyboard should not display (#2598) +* storage.List doesn't return complete URI on Android for "content:" scheme (#2619) +* Last word of the line and first word of the next line are joined in markdown parse (#2647) +* Support for building `cmd/fyne` on Windows arm64 +* Fixed FreeBSD requiring installed glfw library dependency (#1928) +* Apple M1: error when using mouse drag to resize window (#2188) +* Struct binding panics in reload with slice field (#2607) +* File Dialog favourites can break for certain locations (#2595) +* Define user friendly names for Android Apps (#2653) +* Entry validator not updating if content is changed via data binding after SetContent (#2639) +* CenterOnScreen not working for FixedSize Window (#2550) +* Panic in boundStringListItem.Get() (#2643) +* Can't set an app/window icon to be an svg. (#1196) +* SetFullScreen(false) can give error (#2588) + + +## 2.1.1 - 22 October 2021 + +### Fixed + +* Fix issue where table could select cells beyond data bound +* Some fast taps could be ignored (#2484) +* iOS app stops re-drawing mid-frame after a while (#950) +* Mobile simulation mode did not work on Apple M1 computers +* TextGrid background color can show gaps in render (#2493) +* Fix alignment of files in list view of file dialog +* Crash setting visible window on macOS to fixed size (#2488) +* fyne bundle ignores -name flag in windows (#2395) +* Lines with nil colour would crash renderer +* Android -nm tool not found with NDK 23 (#2498) +* Runtime panic because out of touchID (#2407) +* Long text in Select boxes overflows out of the box (#2522) +* Calling SetText on Label may not refresh correctly +* Menu can be triggered by # key but not always Alt +* Cursor position updates twice with delay (#2525) +* widgets freeze after being in background and then a crash upon pop-up menu (#2536) +* too many Refresh() calls may now cause visual artifacts in the List widget (#2548) +* Entry.SetText may panic if called on a multiline entry with selected text (#2482) +* TextGrid not always drawing correctly when resized (#2501) + + +## 2.1.0 - 17 September 2021 + +### Added + +* DocTabs container for handling multiple open files +* Lifecycle API for handling foreground, background and other event +* Add RichText widget and Markdown parser +* Add TabWidth to TextStyle to specify tab size in spaces +* Add CheckGroup widget for multi-select +* Add FyneApp.toml metadata file to ease build commands +* Include http and https in standard repositories +* Add selection color to themes +* Include baseline information in driver font measurement +* Document storage API (App.Storage().Create() and others) +* Add "App Files" to file dialog for apps that use document storage +* Tab overflow on AppTabs +* Add URI and Unbound type to data bindings +* Add keyboard support for menus, pop-ups and buttons +* Add SimpleRenderer to help make simple widgets (#709) +* Add scroll functions for List, Table, Tree (#1892) +* Add selection and disabling to MenuItem +* Add Alignment to widget.Select (#2329) +* Expose ScanCode for keyboard events originating from hardware (#1523) +* Support macOS GPU switching (#2423) + +### Changed + +* Focusable widgets are no longer focused on tap, add canvas.Focus(obj) in Tapped handler if required +* Move to background based selection for List, Table and Tree +* Update fyne command line tool to use --posix style parameters +* Switch from gz to xz compression for unix packages +* Performance improvements with line, text and raster rendering +* Items not yet visible can no longer be focused +* Lines can now be drawn down to 1px (instead of 1dp) (#2298) +* Support multiple lines of text on button (#2378) +* Improved text layout speed by caching string size calculations +* Updated to require Go 1.14 so we can use some new features +* Window Resize request is now asynchronous +* Up/Down keys take cursor home/end when on first/last lines respectively + +### Fixed + +* Correctly align text tabs (#1791) +* Mobile apps theme does not match system (#472) +* Toolbar with widget.Label makes the ToolbarAction buttons higher (#2257) +* Memory leaks in renderers and canvases cache maps (#735) +* FileDialog SetFilter does not work on Android devices (#2353) +* Hover fix for List and Tree with Draggable objects +* Line resize can flip slope (#2208) +* Deadlocks when using widgets with data (#2348) +* Changing input type with keyboard visible would not update soft keyboards +* MainMenu() Close item does NOT call function defined in SetCloseIntercept (#2355) +* Entry cursor position with mouse is offset vertically by theme.SizeNameInputBorder (#2387) +* Backspace key is not working on Android AOSP (#1941) +* macOS: 'NSUserNotification' has been deprecated (#1833) +* macOS: Native menu would add new items if refreshed +* iOS builds fail since Go 1.16 +* Re-add support for 32 bit iOS devices, if built with Go 1.14 +* Android builds fail on Apple M1 (#2439) +* SetFullScreen(true) before ShowAndRun fails (#2446) +* Interacting with another app when window.SetFullScreen(true) will cause the application to hide itself. (#2448) +* Sequential writes to preferences does not save to file (#2449) +* Correct Android keyboard handling (#2447) +* MIUI-Android: The widget’s Hyperlink cannot open the URL (#1514) +* Improved performance of data binding conversions and text MinSize + + +## 2.0.4 - 6 August 2021 + +### Changed + +* Disable Form labels when the element it applys to is disabled (#1530) +* Entry popup menu now fires shortcuts so extended widgets can intercept +* Update Android builds to SDK 30 + +### Fixed + +* sendnotification show appID for name on windows (#1940) +* Fix accidental removal of windows builds during cross-compile +* Removing an item from a container did not update layout +* Update title bar on Windows 10 to match OS theme (#2184) +* Tapped triggered after Drag (#2235) +* Improved documentation and example code for file dialog (#2156) +* Preferences file gets unexpectedly cleared (#2241) +* Extra row dividers rendered on using SetColumnWidth to update a table (#2266) +* Fix resizing fullscreen issue +* Fullscreen changes my display resolution when showing a dialog (#1832) +* Entry validation does not work for empty field (#2179) +* Tab support for focus handling missing on mobile +* ScrollToBottom not always scrolling all the way when items added to container.Scroller +* Fixed scrollbar disappearing after changing content (#2303) +* Calling SetContent a second time with the same content will not show +* Drawing text can panic when Color is nil (#2347) +* Optimisations when drawing transparent rectangle or whitespace strings + + +## 2.0.3 - 30 April 2021 + +### Fixed + +* Optimisations for TextGrid rendering +* Data binding with widget.List sometimes crash while scrolling (#2125) +* Fix compilation on FreeBSD 13 +* DataLists should notify only once when change. +* Keyboard will appear on Android in disabled Entry Widget (#2139) +* Save dialog with filename for Android +* form widget can't draw hinttext of appended item. (#2028) +* Don't create empty shortcuts (#2148) +* Install directory for windows install command contains ".exe" +* Fix compilation for Linux Wayland apps +* Fix tab button layout on mobile (#2117) +* Options popup does not move if a SelectEntry widget moves with popup open +* Speed improvements to Select and SelectEntry drop down +* theme/fonts has an apache LICENSE file but it should have SIL OFL (#2193) +* Fix build requirements for target macOS platforms (#2154) +* ScrollEvent.Position and ScrollEvent.AbsolutePosition is 0,0 (#2199) + + +## 2.0.2 - 1 April 2021 + +### Changed + +* Text can now be copied from a disable Entry using keyboard shortcuts + +### Fixed + +* Slider offset position could be incorrect for mobile apps +* Correct error in example code +* When graphics init fails then don't try to continue running (#1593) +* Don't show global settings on mobile in fyne_demo as it's not supported (#2062) +* Empty selection would render small rectangle in Entry +* Do not show validation state for disabled Entry +* dialog.ShowFileSave did not support mobile (#2076) +* Fix issue that storage could not write to files on iOS and Android +* mobile app could crash in some focus calls +* Duplicate symbol error when compiling for Android with NDK 23 (#2064) +* Add internet permission by default for Android apps (#1715) +* Child and Parent support in storage were missing for mobile appps +* Various crashes with Entry and multiline selections (including #1989) +* Slider calls OnChanged for each value between steps (#1748) +* fyne command doesn't remove temporary binary from src (#1910) +* Advanced Color picker on mobile keeps updating values forever after sliding (#2075) +* exec.Command and widget.Button combination not working (#1857) +* After clicking a link on macOS, click everywhere in the app will be linked (#2112) +* Text selection - Shift+Tab bug (#1787) + + +## 2.0.1 - 4 March 2021 + +### Changed + +* An Entry with `Wrapping=fyne.TextWrapOff` no longer blocks scroll events from a parent + +### Fixed + +* Dialog.Resize() has no effect if called before Dialog.Show() (#1863) +* SelectTab does not always correctly set the blue underline to the selected tab (#1872) +* Entry Validation Broken when using Data binding (#1890) +* Fix background colour not applying until theme change +* android runtime error with fyne.dialog (#1896) +* Fix scale calculations for Wayland phones (PinePhone) +* Correct initial state of entry validation +* fix entry widget mouse drag selection when scrolled +* List widget panic when refreshing after changing content length (#1864) +* Fix image caching that was too aggressive on resize +* Pointer and cursor misalignment in widget.Entry (#1937) +* SIGSEGV Sometimes When Closing a Program by Clicking a Button (#1604) +* Advanced Color Picker shows Black for custom primary color as RGBA (#1970) +* Canvas.Focus() before window visible causes application to crash (#1893) +* Menu over Content (#1973) +* Error compiling fyne on Apple M1 arm64 (#1739) +* Cells are not getting draw in correct location after column resize. (#1951) +* Possible panic when selecting text in a widget.Entry (#1983) +* Form validation doesn't enable submit button (#1965) +* Creating a window shows it before calling .Show() and .Hide() does not work (#1835) +* Dialogs are not refreshed correctly on .Show() (#1866) +* Failed creating setting storage : no such directory (#2023) +* Erroneous custom filter types not supported error on mobile (#2012) +* High importance button show no hovered state (#1785) +* List widget does not render all visible content after content data gets shorter (#1948) +* Calling Select on List before draw can crash (#1960) +* Dialog not resizing in newly created window (#1692) +* Dialog not returning to requested size (#1382) +* Entry without scrollable content prevents scrolling of outside scroller (#1939) +* fyne_demo crash after selecting custom Theme and table (#2018) +* Table widget crash when scrolling rapidly (#1887) +* Cursor animation sometimes distorts the text (#1778) +* Extended password entry panics when password revealer is clicked (#2036) +* Data binding limited to 1024 simultaneous operations (#1838) +* Custom theme does not refresh when variant changes (#2006) + + +## 2.0 - 22 January 2021 + +### Changes that are not backward compatible + +These changes may break some apps, please read the +[upgrading doc](https://developer.fyne.io/api/v2.0/upgrading) for more info +The import path is now `fyne.io/fyne/v2` when you are ready to make the update. + +* Coordinate system to float32 + * Size and Position units were changed from int to float32 + * `Text.TextSize` moved to float32 and `fyne.MeasureText` now takes a float32 size parameter + * Removed `Size.Union` (use `Size.Max` instead) + * Added fyne.Delta for difference-based X, Y float32 representation + * DraggedEvent.DraggedX and DraggedY (int, int) to DraggedEvent.Dragged (Delta) + * ScrollEvent.DeltaX and DeltaY (int, int) moved to ScrollEvent.Scrolled (Delta) + +* Theme API update + * `fyne.Theme` moved to `fyne.LegacyTheme` and can be load to a new theme using `theme.FromLegacy` + * A new, more flexible, Theme interface has been created that we encourage developers to use + +* The second parameter of `theme.NewThemedResource` was removed, it was previously ignored +* The desktop.Cursor definition was renamed desktop.StandardCursor to make way for custom cursors +* Button `Style` and `HideShadow` were removed, use `Importance` + +* iOS apps preferences will be lost in this upgrade as we move to more advanced storage +* Dialogs no longer show when created, unless using the ShowXxx convenience methods +* Entry widget now contains scrolling so should no longer be wrapped in a scroll container + +* Removed deprecated types including: + - `dialog.FileIcon` (now `widget.FileIcon`) + - `widget.Radio` (now `widget.RadioGroup`) + - `widget.AccordionContainer` (now `widget.Accordion`) + - `layout.NewFixedGridLayout()` (now `layout.NewGridWrapLayout()`) + - `widget.ScrollContainer` (now `container.Scroll`) + - `widget.SplitContainer` (now `container.Spilt`) + - `widget.Group` (replaced by `widget.Card`) + - `widget.Box` (now `container.NewH/VBox`, with `Children` field moved to `Objects`) + - `widget.TabContainer` and `widget.AppTabs` (now `container.AppTabs`) +* Many deprecated fields have been removed, replacements listed in API docs 1.4 + - for specific information you can browse https://developer.fyne.io/api/v1.4/ + +### Added + +* Data binding API to connect data sources to widgets and sync data + - Add preferences data binding and `Preferences.AddChangeListener` + - Add bind support to `Check`, `Entry`, `Label`, `List`, `ProgressBar` and `Slider` widgets +* Animation API for handling smooth element transitions + - Add animations to buttons, tabs and entry cursor +* Storage repository API for connecting custom file sources + - Add storage functions `Copy`, `Delete` and `Move` for `URI` + - Add `CanRead`, `CanWrite` and `CanList` to storage APIs +* New Theme API for easier customisation of apps + - Add ability for custom themes to support light/dark preference + - Support for custom icons in theme definition + - New `theme.FromLegacy` helper to use old theme API definitions +* Add fyne.Vector for managing x/y float32 coordinates +* Add MouseButtonTertiary for middle mouse button events on desktop +* Add `canvas.ImageScaleFastest` for faster, less precise, scaling +* Add new `dialog.Form` that will phase out `dialog.Entry` +* Add keyboard control for main menu +* Add `Scroll.OnScrolled` event for seeing changes in scroll container +* Add `TextStyle` and `OnSubmitted` to `Entry` widget +* Add support for `HintText` and showing validation errors in `Form` widget +* Added basic support for tab character in `Entry`, `Label` and `TextGrid` + +### Changed + +* Coordinate system is now float32 - see breaking changes above +* ScrollEvent and DragEvent moved to Delta from (int, int) +* Change bundled resources to use more efficient string storage +* Left and Right mouse buttons on Desktop are being moved to `MouseButtonPrimary` and `MouseButtonSecondary` +* Many optimisations and widget performance enhancements + +* Moving to new `container.New()` and `container.NewWithoutLayout()` constructors (replacing `fyne.NewContainer` and `fyne.NewContainerWithoutLayout`) +* Moving storage APIs `OpenFileFromURI`, `SaveFileToURI` and `ListerForURI` to `Reader`, `Writer` and `List` functions + +### Fixed + +* Validating a widget in widget.Form before renderer was created could cause a panic +* Added file and folder support for mobile simulation support (#1470) +* Appending options to a disabled widget.RadioGroup shows them as enabled (#1697) +* Toggling toolbar icons does not refresh (#1809) +* Black screen when slide up application on iPhone (#1610) +* Properly align Label in FormItem (#1531) +* Mobile dropdowns are too low (#1771) +* Cursor does not go down to next line with wrapping (#1737) +* Entry: while adding text beyond visible reagion there is no auto-scroll (#912) + + +## 1.4.3 - 4 January 2021 + +### Fixed + +* Fix crash when showing file open dialog on iPadOS +* Fix possible missing icon on initial show of disabled button +* Capturing a canvas on macOS retina display would not capture full resolution +* Fix the release build flag for mobile +* Fix possible race conditions for canvas capture +* Improvements to `fyne get` command downloader +* Fix tree, so it refreshes visible nodes on Refresh() +* TabContainer Panic when removing selected tab (#1668) +* Incorrect clipping behaviour with nested scroll containers (#1682) +* MacOS Notifications are not shown on subsequent app runs (#1699) +* Fix the behavior when dragging the divider of split container (#1618) + + +## 1.4.2 - 9 December 2020 + +### Added + +* [fyne-cli] Add support for passing custom build tags (#1538) + +### Changed + +* Run validation on content change instead of on each Refresh in widget.Entry + +### Fixed + +* [fyne-cli] Android: allow to specify an inline password for the keystore +* Fixed Card widget MinSize (#1581) +* Fix missing release tag to enable BuildRelease in Settings.BuildType() +* Dialog shadow does not resize after Refresh (#1370) +* Android Duplicate Number Entry (#1256) +* Support older macOS by default - back to 10.11 (#886) +* Complete certification of macOS App Store releases (#1443) +* Fix compilation errors for early stage Wayland testing +* Fix entry.SetValidationError() not working correctly + + +## 1.4.1 - 20 November 2020 + +### Changed + +* Table columns can now be different sizes using SetColumnWidth +* Avoid unnecessary validation check on Refresh in widget.Form + +### Fixed + +* Tree could flicker on mouse hover (#1488) +* Content of table cells could overflow when sized correctly +* file:// based URI on Android would fail to list folder (#1495) +* Images in iOS release were not all correct size (#1498) +* iOS compile failed with Go 1.15 (#1497) +* Possible crash when minimising app containing List on Windows +* File chooser dialog ignores drive Z (#1513) +* Entry copy/paste is crashing on android 7.1 (#1511) +* Fyne package creating invalid windows packages (#1521) +* Menu bar initially doesn't respond to mouse input on macOS (#505) +* iOS: Missing CFBundleIconName and asset catalog (#1504) +* CenterOnScreen causes crash on MacOS when called from goroutine (#1539) +* desktop.MouseHover Button state is not reliable (#1533) +* Initial validation status in widget.Form is not respected +* Fix nil reference in disabled buttons (#1558) + + +## 1.4 - 1 November 2020 + +### Added (highlights) + +* List (#156), Table (#157) and Tree collection Widgets +* Card, FileItem, Separator widgets +* ColorPicker dialog +* User selection of primary colour +* Container API package to ease using layouts and container widgets +* Add input validation +* ListableURI for working with directories etc +* Added PaddedLayout + +* Window.SetCloseIntercept (#467) +* Canvas.InteractiveArea() to indicate where widgets should avoid +* TextFormatter for ProgressBar +* FileDialog.SetLocation() (#821) +* Added dialog.ShowFolderOpen (#941) +* Support to install on iOS and android with 'fyne install' +* Support asset bundling with go:generate +* Add fyne release command for preparing signed apps +* Add keyboard and focus support to Radio and Select widgets + +### Changed + +* Theme update - new blue highlight, move buttons to outline +* Android SDK target updated to 29 +* Mobile log entries now start "Fyne" instead of "GoLog" +* Don't expand Select to its largest option (#1247) +* Button.HideShadow replaced by Button.Importance = LowImportance + +* Deprecate NewContainer in favour of NewContainerWithoutLayout +* Deprecate HBox and VBox in favour of new container APIs +* Move Container.AddObject to Container.Add matching Container.Remove +* Start move from widget.TabContainer to container.AppTabs +* Replace Radio with RadioGroup +* Deprecate WidgetRenderer.BackgroundColor + +### Fixed + +* Support focus traversal in dialog (#948), (#948) +* Add missing AbsolutePosition in some mouse events (#1274) +* Don't let scrollbar handle become too small +* Ensure tab children are resized before being shown (#1331) +* Don't hang if OpenURL loads browser (#1332) +* Content not filling dialog (#1360) +* Overlays not adjusting on orientation change in mobile (#1334) +* Fix missing key events for some keypad keys (#1325) +* Issue with non-english folder names in Linux favourites (#1248) +* Fix overlays escaping screen interactive bounds (#1358) +* Key events not blocked by overlays (#814) +* Update scroll container content if it is changed (#1341) +* Respect SelectEntry datta changes on refresh (#1462) +* Incorrect SelectEntry dropdown button position (#1361) +* don't allow both single and double tap events to fire (#1381) +* Fix issue where long or tall images could jump on load (#1266, #1432) +* Weird behaviour when resizing or minimizing a ScrollContainer (#1245) +* Fix panic on NewTextGrid().Text() +* Fix issue where scrollbar could jump after mousewheel scroll +* Add missing raster support in software render +* Respect GOOS/GOARCH in fyne command utilities +* BSD support in build tools +* SVG Cache could return the incorrect resource (#1479) + +* Many optimisations and widget performance enhancements +* Various fixes to file creation and saving on mobile devices + + +## 1.3.3 - 10 August 2020 + +### Added + +* Use icons for file dialog favourites (#1186) +* Add ScrollContainer ScrollToBottom and ScrollToTop + +### Changed + +* Make file filter case sensitive (#1185) + +### Fixed + +* Allow popups to create dialogs (#1176) +* Use default cursor for dragging scrollbars (#1172) +* Correctly parse SVG files with missing X/Y for rect +* Fix visibility of Entry placeholder when text is set (#1193) +* Fix encoding issue with Windows notifications (#1191) +* Fix issue where content expanding on Windows could freeze (#1189) +* Fix errors on Windows when reloading Fyne settings (#1165) +* Dialogs not updating theme correctly (#1201) +* Update the extended progressbar on refresh (#1219) +* Segfault if font fails (#1200) +* Slider rendering incorrectly when window maximized (#1223) +* Changing form label not refreshed (#1231) +* Files and folders starting "." show no name (#1235) + + +## 1.3.2 - 11 July 2020 + +### Added + +* Linux packaged apps now include a Makefile to aid install + +### Changed + +* Fyne package supports specific architectures for Android +* Reset missing textures on refresh +* Custom confirm callbacks now called on implicitly shown dialogs +* SelectEntry can update drop-down list during OnChanged callback +* TextGrid whitespace color now matches theme changes +* Order of Window Resize(), SetFixedSize() and CenterOnScreen() does no matter before Show() +* Containers now refresh their visuals as well as their Children on Refresh() + +### Fixed + +* Capped StrokeWidth on canvas.Line (#831) +* Canvas lines, rectangles and circles do not resize and refresh correctly +* Black flickering on resize on MacOS and OS X (possibly not on Catalina) (#1122) +* Crash when resizing window under macOS (#1051, #1140) +* Set SetFixedSize to true, the menus are overlapped (#1105) +* Ctrl+v into text input field crashes app. Presumably clipboard is empty (#1123, #1132) +* Slider default value doesn't stay inside range (#1128) +* The position of window is changed when status change from show to hide, then to show (#1116) +* Creating a windows inside onClose handler causes Fyne to panic (#1106) +* Backspace in entry after SetText("") can crash (#1096) +* Empty main menu causes panic (#1073) +* Installing using `fyne install` on Linux now works on distributions that don't use `/usr/local` +* Fix recommendations from staticcheck +* Unable to overwrite file when using dialog.ShowFileSave (#1168) + + +## 1.3 - 5 June 2020 + +### Added + +* File open and save dialogs (#225) +* Add notifications support (#398) +* Add text wrap support (#332) +* Add Accordion widget (#206) +* Add TextGrid widget (#115) +* Add SplitContainer widget (#205) +* Add new URI type and handlers for cross-platform data access +* Desktop apps can now create splash windows +* Add ScaleMode to images, new ImageScalePixels feature for retro graphics +* Allow widgets to influence mouse cursor style (#726) +* Support changing the text on form submit/cancel buttons +* Support reporting CapsLock key events (#552) +* Add OnClosed callback for Dialog +* Add new image test helpers for validating render output +* Support showing different types of soft keyboard on mobile devices (#971, #975) + +### Changed + +* Upgraded underlying GLFW library to fix various issues (#183, #61) +* Add submenu support and hover effects (#395) +* Default to non-premultiplied alpha (NRGBA) across toolkit +* Rename FixedGridLayout to GridWrapLayout (deprecate old API) (#836) +* Windows redraw and animations continue on window resize and move +* New...PopUp() methods are being replaced by Show...Popup() or New...Popup().Show() +* Apps started on a goroutine will now panic as this is not supported +* On Linux apps now simulate 120DPI instead of 96DPI +* Improved fyne_settings scale picking user interface +* Reorganised fyne_demo to accommodate growing collection of widgets and containers +* Rendering now happens on a different thread to events for more consistent drawing +* Improved text selection on mobile devices + +### Fixed (highlights) + +* Panic when trying to paste empty clipboard into entry (#743) +* Scale does not match user configuration in Windows 10 (#635) +* Copy/Paste not working on Entry Field in Windows OS (#981) +* Select widgets with many options overflow UI without scrolling (#675) +* android: typing in entry expands only after full refresh (#972) +* iOS app stops re-drawing mid frame after a while (#950) +* Too many successive GUI updates do not properly update the view (904) +* iOS apps would not build using Apple's new certificates +* Preserve aspect ratio in SVG stroke drawing (#976) +* Fixed many race conditions in widget data handling +* Various crashes and render glitches in extended widgets +* Fix security issues reported by gosec (#742) + + +## 1.2.4 - 13 April 2020 + +### Added + + * Added Direction field to ScrollContainer and NewHScrollContainer, NewVScrollContainer constructors (#763) + * Added Scroller.SetMinSize() to enable better defaults for scrolled content + * Added "fyne vendor" subcommand to help packaging fyne dependencies in projects + * Added "fyne version" subcommand to help with bug reporting (#656) + * Clipboard (cut/copy/paste) is now supported on iOS and Android (#414) + * Preferences.RemoveValue() now allows deletion of a stored user preference + +### Changed + + * Report keys based on name not key code - fixes issue with shortcuts with AZERTY (#790) + +### Fixed + + * Mobile builds now support go modules (#660) + * Building for mobile would try to run desktop build first + * Mobile apps now draw the full safe area on a screen (#799) + * Preferences were not stored on mobile apps (#779) + * Window on Windows is not controllable after exiting FullScreen mode (#727) + * Soft keyboard not working on some Samsung/LG smart phones (#787) + * Selecting a tab on extended TabContainer doesn't refresh button (#810) + * Appending tab to empty TabContainer causes divide by zero on mobile (#820) + * Application crashes on startup (#816) + * Form does not always update on theme change (#842) + + +## 1.2.3 - 2 March 2020 + +### Added + + * Add media and volume icons to default themes (#649) + * Add Canvas.PixelCoordinateForPosition to find pixel locations if required + * Add ProgressInfinite dialog + +### Changed + + * Warn if -executable or -sourceDir flags are used for package on mobile (#652) + * Update scale based on device for mobile apps + * Windows without a title will now be named "Fyne Application" + * Revert fix to quit mobile apps - this is not allowed in guidelines + +### Fixed + + * App.UniqueID() did not return current app ID + * Fyne package ignored -name flag for ios and android builds (#657) + * Possible crash when appending tabs to TabContainer + * FixedSize windows not rescaling when dragged between monitors (#654) + * Fix issues where older Android devices may not background or rotate (#677) + * Crash when setting theme before window content set (#688) + * Correct form extend behaviour (#694) + * Select drop-down width is wrong if the drop-down is too tall for the window (#706) + + +## 1.2.2 - 29 January 2020 + +### Added + +* Add SelectedText() function to Entry widget +* New mobile.Device interface exposing ShowVirtualKeyboard() (and Hide...) + +### Changed + +* Scale calculations are now relative to system scale - the default "1" matches the system +* Update scale on Linux to be "auto" by default (and numbers are relative to 96DPI standard) (#595) +* When auto scaling check the monitor in the middle of the window, not top left +* bundled files now have a standard header to optimise some tools like go report card +* Shortcuts are now handled by the event queue - fixed possible deadlock + +### Fixed + +* Scroll horizontally when holding shift key (#579) +* Updating text and calling refresh for widget doesn't work (#607) +* Corrected visual behaviour of extended widgets including Entry, Select, Check, Radio and Icon (#615) +* Entries and Selects that are extended would crash on right click. +* PasswordEntry created from Entry with Password = true has no revealer +* Dialog width not always sufficient for title +* Pasting unicode characters could panic (#597) +* Setting theme before application start panics on macOS (#626) +* MenuItem type conflicts with other projects (#632) + + +## 1.2.1 - 24 December 2019 + +### Added + +* Add TouchDown, TouchUp and TouchCancel API in driver/mobile for device specific events +* Add support for adding and removing tabs from a tab container (#444) + +### Fixed + +* Issues when settings changes may not be monitored (#576) +* Layout of hidden tab container contents on mobile (#578) +* Mobile apps would not quit when Quit() was called (#580) +* Shadows disappeared when theme changes (#589) +* iOS apps could stop rendering after many refreshes (#584) +* Fyne package could fail on Windows (#586) +* Horizontal only scroll container may not refresh using scroll wheel + + +## 1.2 - 12 December 2019 + +### Added + +* Mobile support - iOS and Android, including "fyne package" command +* Support for OpenGL ES and embedded linux +* New BaseWidget for building custom widgets +* Support for diagonal gradients +* Global settings are now saved and can be set using the new fyne_settings app +* Support rendering in Go playground using playground.Render() helpers +* "fyne install" command to package and install apps on the local computer +* Add horizontal scrolling to ScrollContainer +* Add preferences API +* Add show/hide password icon when created from NewPasswordEntry +* Add NewGridLayoutWithRows to specify a grid layout with a set number of rows +* Add NewAdaptiveGridLayout which uses a column grid layout when horizontal and rows in vertical + + +### Changed + +* New Logo! Thanks to Storm for his work on this :) +* Applications no longer have a default (Fyne logo) icon +* Input events now execute one at a time to maintain the correct order +* Button and other widget callbacks no longer launch new goroutines +* FYNE_THEME and FYNE_SCALE are now overrides to the global configuration +* The first opened window no longer exits the app when closed (unless none others are open or Window.SetMaster() is called) +* "fyne package" now defaults icon to "Icon.png" so the parameter is optional +* Calling ExtendBaseWidget() sets up the renderer for extended widgets +* Entry widget now has a visible Disabled state, ReadOnly has been deprecated +* Bundled images optimised to save space +* Optimise rendering to reduce refresh on TabContainer and ScrollContainer + + +### Fixed + +* Correct the color of Entry widget cursor if theme changes +* Error where widgets created before main() function could crash (#490) +* App.Run panics if called without a window (#527) +* Support context menu for disabled entry widgets (#488) +* Fix issue where images using fyne.ImageFillOriginal may not show initially (#558) + + +## 1.1.2 - 12 October 2019 + +### Added + +### Changed + +* Default scale value for canvases is now 1.0 instead of Auto (DPI based) + +### Fixed + +* Correct icon name in linux packages +* Fullscreen before showing a window works again +* Incorrect MinSize of FixedGrid layout in some situations +* Update text size on theme change +* Text handling crashes (#411, #484, #485) +* Layout of image only buttons +* TabItem.Content changes are reflected when refreshing TabContainer (#456) + +## 1.1.1 - 17 August 2019 + +### Added + +* Add support for custom Windows manifest files in fyne package + +### Changed + +* Dismiss non-modal popovers on secondary tap +* Only measure visible objects in layouts and minSize calculations (#343) +* Don't propagate show/hide in the model - allowing children of tabs to remain hidden +* Disable cut/copy for password fields +* Correctly calculate grid layout minsize as width changes +* Select text at end of line when double tapping beyond width + +### Fixed + +* Scale could be too large on macOS Retina screens +* Window with fixed size changes size when un-minimized on Windows (#300) +* Setting text on a label could crash if it was not yet shown (#381) +* Multiple Entry widgets could have selections simultaneously (#341) +* Hover effect of radio widget too low (#383) +* Missing shadow on Select widget +* Incorrect rendering of subimages within Image object +* Size calculation caches could be skipped causing degraded performance + + +## 1.1 - 1 July 2019 + +### Added + +* Menubar and PopUpMenu (#41) +* PopUp widgets (regular and modal) and canvas overlay support (#242) +* Add gradient (linear and radial) to canvas +* Add shadow support for overlays, buttons and scrollcontainer +* Text can now be selected (#67) +* Support moving through inputs with Tab / Shift-Tab (#82) +* canvas.Capture() to save the content of a canvas +* Horizontal layout for widget.Radio +* Select widget (#21) +* Add support for disabling widgets (#234) +* Support for changing icon color (#246) +* Button hover effect +* Pointer drag event to main API +* support for desktop mouse move events +* Add a new "hints" build tag that can suggest UI improvements + +### Changed + +* TabContainer tab location can now be set with SetTabLocation() +* Dialog windows now appear as modal popups within a window +* Don't add a button bar to a form if it has no buttons +* Moved driver/gl package to internal/driver/gl +* Clicking/Tapping in an entry will position the cursor +* A container with no layout will not change the position or size of its content +* Update the fyne_demo app to reflect the expanding feature set + +### Fixed + +* Allow scrollbars to be dragged (#133) +* Unicode char input with Option key on macOS (#247) +* Resizng fixed size windows (#248) +* Fixed various bugs in window sizing and padding +* Button icons do not center align if label is empty (#284) + + +## 1.0.1 - 20 April 2019 + +### Added + +* Support for go modules +* Transparent backgrounds for widgets +* Entry.OnCursorChanged() +* Radio.Append() and Radio.SetSelected() (#229) + +### Changed + +* Clicking outside a focused element will unfocus it +* Handle key repeat for non-runes (#165) + +### Fixed + +* Remove duplicate options from a Radio widget (#230) +* Issue where paste shortcut is not called for Ctrl-V keyboard combination +* Cursor position when clearing text in Entry (#214) +* Antialias of lines and circles (fyne-io/examples#14) +* Crash on centering of windows (#220) +* Possible crash when closing secondary windows +* Possible crash when showing dialog +* Initial visibility of scroll bar in ScrollContainer +* Setting window icon when different from app icon. +* Possible panic on app.Quit() (#175) +* Various caches and race condition issues (#194, #217, #209). + + +## 1.0 - 19 March 2019 + +The first major release of the Fyne toolkit delivers a stable release of the +main functionality required to build basic GUI applications across multiple +platforms. + +### Features + +* Canvas API (rect, line, circle, text, image) +* Widget API (box, button, check, entry, form, group, hyperlink, icon, label, progress bar, radio, scroller, tabs and toolbar) +* Light and dark themes +* Pointer, key and shortcut APIs (generic and desktop extension) +* OpenGL driver for Linux, macOS and Windows +* Tools for embedding data and packaging releases + diff --git a/vendor/fyne.io/fyne/v2/CODE_OF_CONDUCT.md b/vendor/fyne.io/fyne/v2/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..ccb6229 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at info@fyne.io. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/vendor/fyne.io/fyne/v2/CONTRIBUTING.md b/vendor/fyne.io/fyne/v2/CONTRIBUTING.md new file mode 100644 index 0000000..48aeeac --- /dev/null +++ b/vendor/fyne.io/fyne/v2/CONTRIBUTING.md @@ -0,0 +1,63 @@ +Thanks very much for your interest in contributing to Fyne! +The community is what makes this project successful and we are glad to welcome you on board. + +There are various ways to contribute, perhaps the following helps you know how to get started. + +## Reporting a bug + +If you've found something wrong we want to know about it, please help us understand the problem so we can resolve it. + +1. Check to see if this already is recorded, if so add some more information [issue list](https://github.com/fyne-io/fyne/issues) +2. If not then create a new issue using the [bug report template](https://github.com/fyne-io/fyne/issues/new?assignees=&labels=&template=bug_report.md&title=) +3. Stay involved in the conversation on the issue as it is triaged and progressed. + + +## Fixing an issue + +Great! You found an issue and figured you can fix it for us. +If you can follow these steps then your code should get accepted fast. + +1. Read through the "Contributing Code" section further down this page. +2. Write a unit test to show it is broken. +3. Create the fix and you should see the test passes. +4. Run the tests and make sure everything still works as expected using `go test ./...`. +5. [Open a PR](https://github.com/fyne-io/fyne/compare) and work through the review checklist. + + +## Adding a feature + +It's always good news to hear that people want to contribute functionality. +But first of all check that it fits within our [Vision](https://github.com/fyne-io/fyne/wiki/Vision) and if we are already considering it on our [Roadmap](https://github.com/fyne-io/fyne/wiki/Roadmap). +If you're not sure then you should join our #fyne-contributors channel on the [Gophers Slack server](https://gophers.slack.com/app_redirect?channel=fyne-contributors). + +Once you are ready to code then the following steps should give you a smooth process: + +1. Read through the [Contributing Code](#contributing-code) section further down this page. +2. Think about how you would structure your code and how it can be tested. +3. Write some code and enjoy the ease of writing Go code for even a complex project :). +4. Run the tests and make sure everything still works as expected using `go test ./...`. +5. [Open a PR](https://github.com/fyne-io/fyne/compare) and work through the review checklist. + + +# Contributing Code + +We aim to maintain a very high standard of code, through design, test and implementation. +To manage this we have various checks and processes in place that everyone should follow, including: + +* We use the Go standard format (with tabs not spaces) - you can run `gofmt` before committing +* Imports should be ordered according to the GoImports spec - you can use the `goimports` tool instead of `gofmt`. +* Everything should have a unit test attached (as much as possible, to keep our coverage up) + +For detailed Code style, check [Contributing](https://github.com/fyne-io/fyne/wiki/Contributing#code-style) in our wiki please. + +# Decision Process + +The following points apply to our decision making process: + +* Any decisions or votes will be opened on the #fyne-votes Slack channel and follows lazy consensus. +* Any contributors not responding in 4 days will be deemed in agreement. +* Any PR that has not been responded to within 7 days can be automatically approved. +* No functionality will be added unless at least 2 developers agree it belongs. + +Bear in mind that this is a cross platform project so any new features would normally +be required to work on multiple desktop and mobile platforms. diff --git a/vendor/fyne.io/fyne/v2/LICENSE b/vendor/fyne.io/fyne/v2/LICENSE new file mode 100644 index 0000000..c6cb658 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (C) 2018 Fyne.io developers (see AUTHORS) +All rights reserved. + + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Fyne.io nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/vendor/fyne.io/fyne/v2/README.md b/vendor/fyne.io/fyne/v2/README.md new file mode 100644 index 0000000..c8a88fd --- /dev/null +++ b/vendor/fyne.io/fyne/v2/README.md @@ -0,0 +1,184 @@ +

+ Go API Reference + Latest Release + Join us on Slack +
+ Code Status + Build Status + Coverage Status +

+ +# About + +[Fyne](https://fyne.io) is an easy-to-use UI toolkit and app API written in Go. +It is designed to build applications that run on desktop and mobile devices with a +single codebase. + +# Prerequisites + +To develop apps using Fyne you will need Go version 1.17 or later, a C compiler and your system's development tools. +If you're not sure if that's all installed or you don't know how then check out our +[Getting Started](https://fyne.io/develop/) document. + +Using the standard go tools you can install Fyne's core library using: + + go get fyne.io/fyne/v2@latest + +After importing a new module, run the following command before compiling the code for the first time. Avoid running it before writing code that uses the module to prevent accidental removal of dependencies: + + go mod tidy + +# Widget demo + +To run a showcase of the features of Fyne execute the following: + + go install fyne.io/fyne/v2/cmd/fyne_demo@latest + fyne_demo + +And you should see something like this (after you click a few buttons): + +

+ Fyne Demo Dark Theme +

+ +Or if you are using the light theme: + +

+ Fyne Demo Light Theme +

+ +And even running on a mobile device: + +

+ Fyne Demo Mobile Light Theme +

+ +# Getting Started + +Fyne is designed to be really easy to code with. +If you have followed the prerequisite steps above then all you need is a +Go IDE (or a text editor). + +Open a new file and you're ready to write your first app! + +```go +package main + +import ( + "fyne.io/fyne/v2/app" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/widget" +) + +func main() { + a := app.New() + w := a.NewWindow("Hello") + + hello := widget.NewLabel("Hello Fyne!") + w.SetContent(container.NewVBox( + hello, + widget.NewButton("Hi!", func() { + hello.SetText("Welcome :)") + }), + )) + + w.ShowAndRun() +} +``` + +And you can run that simply as: + + go run main.go + +> [!NOTE] +> The first compilation of Fyne on Windows _can_ take up to 10 minutes, depending on your hardware. Subsequent builds will be fast. + +It should look like this: + +
+ + +
+ Fyne Hello Dark Theme + + Fyne Hello Dark Theme +
+
+ +## Run in mobile simulation + +There is a helpful mobile simulation mode that gives a hint of how your app would work on a mobile device: + + go run -tags mobile main.go + +Another option is to use `fyne` command, see [Packaging for mobile](#packaging-for-mobile). + +# Installing + +Using `go install` will copy the executable into your go `bin` dir. +To install the application with icons etc into your operating system's standard +application location you can use the fyne utility and the "install" subcommand. + + go install fyne.io/tools/cmd/fyne@latest + fyne install + +# Packaging for mobile + +To run on a mobile device it is necessary to package up the application. +To do this we can use the fyne utility "package" subcommand. +You will need to add appropriate parameters as prompted, but the basic command is shown below. +Once packaged you can install using the platform development tools or the fyne "install" subcommand. + + fyne package -os android -appID my.domain.appname + fyne install -os android + +The built Android application can run either in a real device or an Android emulator. +However, building for iOS is slightly different. +If the "-os" argument is "ios", it is build only for a real iOS device. +Specify "-os" to "iossimulator" allows the application be able to run in an iOS simulator: + + fyne package -os ios -appID my.domain.appname + fyne package -os iossimulator -appID my.domain.appname + +# Preparing a release + +Using the fyne utility "release" subcommand you can package up your app for release +to app stores and market places. Make sure you have the standard build tools installed +and have followed the platform documentation for setting up accounts and signing. +Then you can execute something like the following, notice the `-os ios` parameter allows +building an iOS app from macOS computer. Other combinations work as well :) + + $ fyne release -os ios -certificate "Apple Distribution" -profile "My App Distribution" -appID "com.example.myapp" + +The above command will create a '.ipa' file that can then be uploaded to the iOS App Store. + +# Documentation + +More documentation is available at the [Fyne developer website](https://developer.fyne.io/) or on [pkg.go.dev](https://pkg.go.dev/fyne.io/fyne/v2?tab=doc). + +# Examples + +You can find many example applications in the [examples repository](https://github.com/fyne-io/examples/). +Alternatively a list of applications using fyne can be found at [our website](https://apps.fyne.io/). + +# Shipping the Fyne Toolkit + +All Fyne apps will work without pre-installed libraries, this is one reason the apps are so portable. +However, if looking to support Fyne in a bigger way on your operating system then you can install some utilities that help to make a more complete experience. + +## Additional apps + +It is recommended that you install the following additional apps: + +| app | go install | description | +| ------------- | ----------------------------------- | ---------------------------------------------------------------------- | +| fyne_settings | `fyne.io/fyne/v2/cmd/fyne_settings` | A GUI for managing your global Fyne settings like theme and scaling | +| apps | `github.com/fyne-io/apps` | A graphical installer for the Fyne apps listed at https://apps.fyne.io | + +These are optional applications but can help to create a more complete desktop experience. + +## FyneDesk (Linux / BSD) + +To go all the way with Fyne on your desktop / laptop computer you could install [FyneDesk](https://github.com/fyshos/fynedesk) as well :) + +![FyneDesk screenshopt in dark mode](https://fyshos.com/img/desktop.png) diff --git a/vendor/fyne.io/fyne/v2/SECURITY.md b/vendor/fyne.io/fyne/v2/SECURITY.md new file mode 100644 index 0000000..5976dc4 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/SECURITY.md @@ -0,0 +1,15 @@ +# Security Policy + +## Supported Versions + +Minor releases will receive security updates and fixes until the next minor or major release. + +| Version | Supported | +| ------- | ------------------ | +| 2.6.x | :white_check_mark: | +| < 2.6.0 | :x: | + +## Reporting a Vulnerability + +Report security vulnerabilities using the [advisories](https://github.com/fyne-io/fyne/security/advisories) page on GitHub. +The team of core developers will evaluate and address the issue as appropriate. diff --git a/vendor/fyne.io/fyne/v2/animation.go b/vendor/fyne.io/fyne/v2/animation.go new file mode 100644 index 0000000..2883774 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/animation.go @@ -0,0 +1,84 @@ +package fyne + +import "time" + +// AnimationCurve represents an animation algorithm for calculating the progress through a timeline. +// Custom animations can be provided by implementing the "func(float32) float32" definition. +// The input parameter will start at 0.0 when an animation starts and travel up to 1.0 at which point it will end. +// A linear animation would return the same output value as is passed in. +type AnimationCurve func(float32) float32 + +// AnimationRepeatForever is an AnimationCount value that indicates it should not stop looping. +// +// Since: 2.0 +const AnimationRepeatForever = -1 + +var ( + // AnimationEaseInOut is the default easing, it starts slowly, accelerates to the middle and slows to the end. + // + // Since: 2.0 + AnimationEaseInOut = animationEaseInOut + // AnimationEaseIn starts slowly and accelerates to the end. + // + // Since: 2.0 + AnimationEaseIn = animationEaseIn + // AnimationEaseOut starts at speed and slows to the end. + // + // Since: 2.0 + AnimationEaseOut = animationEaseOut + // AnimationLinear is a linear mapping for animations that progress uniformly through their duration. + // + // Since: 2.0 + AnimationLinear = animationLinear +) + +// Animation represents an animated element within a Fyne canvas. +// These animations may control individual objects or entire scenes. +// +// Since: 2.0 +type Animation struct { + AutoReverse bool + Curve AnimationCurve + Duration time.Duration + RepeatCount int + Tick func(float32) +} + +// NewAnimation creates a very basic animation where the callback function will be called for every +// rendered frame between [time.Now] and the specified duration. The callback values start at 0.0 and +// will be 1.0 when the animation completes. +// +// Since: 2.0 +func NewAnimation(d time.Duration, fn func(float32)) *Animation { + return &Animation{Duration: d, Tick: fn} +} + +// Start registers the animation with the application run-loop and starts its execution. +func (a *Animation) Start() { + CurrentApp().Driver().StartAnimation(a) +} + +// Stop will end this animation and remove it from the run-loop. +func (a *Animation) Stop() { + CurrentApp().Driver().StopAnimation(a) +} + +func animationEaseIn(val float32) float32 { + return val * val +} + +func animationEaseInOut(val float32) float32 { + if val <= 0.5 { + return val * val * 2 + } + + return -1 + (4-val*2)*val +} + +func animationEaseOut(val float32) float32 { + return val * (2 - val) +} + +func animationLinear(val float32) float32 { + return val +} diff --git a/vendor/fyne.io/fyne/v2/app.go b/vendor/fyne.io/fyne/v2/app.go new file mode 100644 index 0000000..ef02a6e --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app.go @@ -0,0 +1,145 @@ +package fyne + +import ( + "net/url" + "sync/atomic" +) + +// An App is the definition of a graphical application. +// Apps can have multiple windows, by default they will exit when all windows +// have been closed. This can be modified using SetMaster or SetCloseIntercept. +// To start an application you need to call Run somewhere in your main function. +// Alternatively use the [fyne.io/fyne/v2.Window.ShowAndRun] function for your main window. +type App interface { + // Create a new window for the application. + // The first window to open is considered the "master" and when closed + // the application will exit. + NewWindow(title string) Window + + // Open a URL in the default browser application. + OpenURL(url *url.URL) error + + // Icon returns the application icon, this is used in various ways + // depending on operating system. + // This is also the default icon for new windows. + Icon() Resource + + // SetIcon sets the icon resource used for this application instance. + SetIcon(Resource) + + // Run the application - this starts the event loop and waits until [App.Quit] + // is called or the last window closes. + // This should be called near the end of a main() function as it will block. + Run() + + // Calling Quit on the application will cause the application to exit + // cleanly, closing all open windows. + // This function does no thing on a mobile device as the application lifecycle is + // managed by the operating system. + Quit() + + // Driver returns the driver that is rendering this application. + // Typically not needed for day to day work, mostly internal functionality. + Driver() Driver + + // UniqueID returns the application unique identifier, if set. + // This must be set for use of the [App.Preferences]. see [NewWithID]. + UniqueID() string + + // SendNotification sends a system notification that will be displayed in the operating system's notification area. + SendNotification(*Notification) + + // Settings return the globally set settings, determining theme and so on. + Settings() Settings + + // Preferences returns the application preferences, used for storing configuration and state + Preferences() Preferences + + // Storage returns a storage handler specific to this application. + Storage() Storage + + // Lifecycle returns a type that allows apps to hook in to lifecycle events. + // + // Since: 2.1 + Lifecycle() Lifecycle + + // Metadata returns the application metadata that was set at compile time. + // The items of metadata are available after "fyne package" or when running "go run" + // Building with "go build" may cause this to be unavailable. + // + // Since: 2.2 + Metadata() AppMetadata + + // CloudProvider returns the current app cloud provider, + // if one has been registered by the developer or chosen by the user. + // + // Since: 2.3 + CloudProvider() CloudProvider // get the (if any) configured provider + + // SetCloudProvider allows developers to specify how this application should integrate with cloud services. + // See [fyne.io/cloud] package for implementation details. + // + // Since: 2.3 + SetCloudProvider(CloudProvider) // configure cloud for this app + + // Clipboard returns the system clipboard. + // + // Since: 2.6 + Clipboard() Clipboard +} + +var app atomic.Pointer[App] + +// SetCurrentApp is an internal function to set the app instance currently running. +func SetCurrentApp(current App) { + app.Store(¤t) +} + +// CurrentApp returns the current application, for which there is only 1 per process. +func CurrentApp() App { + val := app.Load() + if val == nil { + LogError("Attempt to access current Fyne app when none is started", nil) + return nil + } + return *val +} + +// AppMetadata captures the build metadata for an application. +// +// Since: 2.2 +type AppMetadata struct { + // ID is the unique ID of this application, used by many distribution platforms. + ID string + // Name is the human friendly name of this app. + Name string + // Version represents the version of this application, normally following semantic versioning. + Version string + // Build is the build number of this app, some times appended to the version number. + Build int + // Icon contains, if present, a resource of the icon that was bundled at build time. + Icon Resource + // Release if true this binary was build in release mode + // Since: 2.3 + Release bool + // Custom contain the custom metadata defined either in FyneApp.toml or on the compile command line + // Since: 2.3 + Custom map[string]string + // Migrations allows an app to opt into features before they are standard + // Since: 2.6 + Migrations map[string]bool +} + +// Lifecycle represents the various phases that an app can transition through. +// +// Since: 2.1 +type Lifecycle interface { + // SetOnEnteredForeground hooks into the app becoming foreground and gaining focus. + SetOnEnteredForeground(func()) + // SetOnExitedForeground hooks into the app losing input focus and going into the background. + SetOnExitedForeground(func()) + // SetOnStarted hooks into an event that says the app is now running. + SetOnStarted(func()) + // SetOnStopped hooks into an event that says the app is no longer running. + SetOnStopped(func()) +} diff --git a/vendor/fyne.io/fyne/v2/app/app.go b/vendor/fyne.io/fyne/v2/app/app.go new file mode 100644 index 0000000..ceddf3c --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/app.go @@ -0,0 +1,190 @@ +// Package app provides app implementations for working with Fyne graphical interfaces. +// The fastest way to get started is to call app.New() which will normally load a new desktop application. +// If the "ci" tag is passed to go (go run -tags ci myapp.go) it will run an in-memory application. +package app // import "fyne.io/fyne/v2/app" + +import ( + "strconv" + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal" + "fyne.io/fyne/v2/internal/app" + intRepo "fyne.io/fyne/v2/internal/repository" + "fyne.io/fyne/v2/storage" + "fyne.io/fyne/v2/storage/repository" +) + +// Declare conformity with App interface +var _ fyne.App = (*fyneApp)(nil) + +type fyneApp struct { + driver fyne.Driver + clipboard fyne.Clipboard + icon fyne.Resource + uniqueID string + + cloud fyne.CloudProvider + lifecycle app.Lifecycle + settings *settings + storage fyne.Storage + prefs fyne.Preferences +} + +func (a *fyneApp) CloudProvider() fyne.CloudProvider { + return a.cloud +} + +func (a *fyneApp) Icon() fyne.Resource { + if a.icon != nil { + return a.icon + } + + if a.Metadata().Icon == nil || len(a.Metadata().Icon.Content()) == 0 { + return nil + } + return a.Metadata().Icon +} + +func (a *fyneApp) SetIcon(icon fyne.Resource) { + a.icon = icon +} + +func (a *fyneApp) UniqueID() string { + if a.uniqueID != "" { + return a.uniqueID + } + if a.Metadata().ID != "" { + return a.Metadata().ID + } + + fyne.LogError("Preferences API requires a unique ID, use app.NewWithID() or the FyneApp.toml ID field", nil) + a.uniqueID = "missing-id-" + strconv.FormatInt(time.Now().Unix(), 10) // This is a fake unique - it just has to not be reused... + return a.uniqueID +} + +func (a *fyneApp) NewWindow(title string) fyne.Window { + return a.driver.CreateWindow(title) +} + +func (a *fyneApp) Run() { + go a.lifecycle.RunEventQueue(a.driver.DoFromGoroutine) + + if !a.driver.Device().IsMobile() { + a.settings.watchSettings() + } + + a.driver.Run() +} + +func (a *fyneApp) Quit() { + for _, window := range a.driver.AllWindows() { + window.Close() + } + + a.driver.Quit() + a.settings.stopWatching() +} + +func (a *fyneApp) Driver() fyne.Driver { + return a.driver +} + +// Settings returns the application settings currently configured. +func (a *fyneApp) Settings() fyne.Settings { + return a.settings +} + +func (a *fyneApp) Storage() fyne.Storage { + return a.storage +} + +func (a *fyneApp) Preferences() fyne.Preferences { + if a.UniqueID() == "" { + fyne.LogError("Preferences API requires a unique ID, use app.NewWithID() or the FyneApp.toml ID field", nil) + } + return a.prefs +} + +func (a *fyneApp) Lifecycle() fyne.Lifecycle { + return &a.lifecycle +} + +func (a *fyneApp) newDefaultPreferences() *preferences { + p := newPreferences(a) + if a.uniqueID != "" { + p.load() + } + return p +} + +func (a *fyneApp) Clipboard() fyne.Clipboard { + return a.clipboard +} + +// New returns a new application instance with the default driver and no unique ID (unless specified in FyneApp.toml) +func New() fyne.App { + if meta.ID == "" { + checkLocalMetadata() // if no ID passed, check if it was in toml + if meta.ID == "" { + internal.LogHint("Applications should be created with a unique ID using app.NewWithID()") + } + } + return NewWithID(meta.ID) +} + +func makeStoreDocs(id string, s *store) *internal.Docs { + if id == "" { + return &internal.Docs{} // an empty impl to avoid crashes + } + if root := s.a.storageRoot(); root != "" { + uri, err := storage.ParseURI(root) + if err != nil { + uri = storage.NewFileURI(root) + } + + exists, err := storage.Exists(uri) + if !exists || err != nil { + err = storage.CreateListable(uri) + if err != nil { + fyne.LogError("Failed to create app storage space", err) + } + } + + root, _ := s.docRootURI() + return &internal.Docs{RootDocURI: root} + } else { + return &internal.Docs{} // an empty impl to avoid crashes + } +} + +func newAppWithDriver(d fyne.Driver, clipboard fyne.Clipboard, id string) fyne.App { + newApp := &fyneApp{uniqueID: id, clipboard: clipboard, driver: d} + fyne.SetCurrentApp(newApp) + + newApp.prefs = newApp.newDefaultPreferences() + newApp.lifecycle.InitEventQueue() + newApp.lifecycle.SetOnStoppedHookExecuted(func() { + if prefs, ok := newApp.prefs.(*preferences); ok { + prefs.forceImmediateSave() + } + }) + + newApp.registerRepositories() // for web this may provide docs / settings + newApp.settings = loadSettings() + store := &store{a: newApp} + store.Docs = makeStoreDocs(id, store) + newApp.storage = store + + httpHandler := intRepo.NewHTTPRepository() + repository.Register("http", httpHandler) + repository.Register("https", httpHandler) + return newApp +} + +// marker interface to pass system tray to supporting drivers +type systrayDriver interface { + SetSystemTrayMenu(*fyne.Menu) + SetSystemTrayIcon(fyne.Resource) + SetSystemTrayWindow(fyne.Window) +} diff --git a/vendor/fyne.io/fyne/v2/app/app_darwin.go b/vendor/fyne.io/fyne/v2/app/app_darwin.go new file mode 100644 index 0000000..3896ef9 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/app_darwin.go @@ -0,0 +1,60 @@ +//go:build !ci && !wasm && !test_web_driver && !mobile && !tinygo + +package app + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Foundation + +#include +#include + +bool isBundled(); +void sendNotification(char *title, char *content); +*/ +import "C" + +import ( + "fmt" + "os/exec" + "strings" + "unsafe" + + "fyne.io/fyne/v2" +) + +func (a *fyneApp) SendNotification(n *fyne.Notification) { + if C.isBundled() { + titleStr := C.CString(n.Title) + defer C.free(unsafe.Pointer(titleStr)) + contentStr := C.CString(n.Content) + defer C.free(unsafe.Pointer(contentStr)) + + C.sendNotification(titleStr, contentStr) + return + } + + fallbackNotification(n.Title, n.Content) +} + +func escapeNotificationString(in string) string { + noSlash := strings.ReplaceAll(in, "\\", "\\\\") + return strings.ReplaceAll(noSlash, "\"", "\\\"") +} + +//export fallbackSend +func fallbackSend(cTitle, cContent *C.char) { + title := C.GoString(cTitle) + content := C.GoString(cContent) + fallbackNotification(title, content) +} + +func fallbackNotification(title, content string) { + template := `display notification "%s" with title "%s"` + script := fmt.Sprintf(template, escapeNotificationString(content), escapeNotificationString(title)) + + err := exec.Command("osascript", "-e", script).Start() + if err != nil { + fyne.LogError("Failed to launch darwin notify script", err) + } +} diff --git a/vendor/fyne.io/fyne/v2/app/app_darwin.m b/vendor/fyne.io/fyne/v2/app/app_darwin.m new file mode 100644 index 0000000..443060c --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/app_darwin.m @@ -0,0 +1,60 @@ +//go:build !ci && !wasm && !test_web_driver && !mobile + +#import +#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101400 || TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR +#import +#endif + +static int notifyNum = 0; + +extern void fallbackSend(char *cTitle, char *cBody); + +bool isBundled() { + return [[NSBundle mainBundle] bundleIdentifier] != nil; +} + +#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101400 || TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR +void doSendNotification(UNUserNotificationCenter *center, NSString *title, NSString *body) { + UNMutableNotificationContent *content = [UNMutableNotificationContent new]; + [content autorelease]; + content.title = title; + content.body = body; + + notifyNum++; + NSString *identifier = [NSString stringWithFormat:@"fyne-notify-%d", notifyNum]; + UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:identifier + content:content trigger:nil]; + + [center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) { + if (error != nil) { + NSLog(@"Could not send notification: %@", error); + } + }]; +} + +void sendNotification(char *cTitle, char *cBody) { + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + NSString *title = [NSString stringWithUTF8String:cTitle]; + NSString *body = [NSString stringWithUTF8String:cBody]; + + UNAuthorizationOptions options = UNAuthorizationOptionAlert; + [center requestAuthorizationWithOptions:options + completionHandler:^(BOOL granted, NSError *_Nullable error) { + if (!granted) { + if (error != NULL) { + NSLog(@"Error asking for permission to send notifications %@", error); + // this happens if our app was not signed, so do it the old way + fallbackSend((char *)[title UTF8String], (char *)[body UTF8String]); + } else { + NSLog(@"Unable to get permission to send notifications"); + } + } else { + doSendNotification(center, title, body); + } + }]; +} +#else +void sendNotification(char *cTitle, char *cBody) { + fallbackSend(cTitle, cBody); +} +#endif diff --git a/vendor/fyne.io/fyne/v2/app/app_desktop_darwin.go b/vendor/fyne.io/fyne/v2/app/app_desktop_darwin.go new file mode 100644 index 0000000..3828d8c --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/app_desktop_darwin.go @@ -0,0 +1,61 @@ +//go:build !ci && !ios && !wasm && !test_web_driver && !mobile + +package app + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Foundation + +#include + +bool isBundled(); +void watchTheme(); +*/ +import "C" + +import ( + "net/url" + "os" + "os/exec" + + "fyne.io/fyne/v2" +) + +func (a *fyneApp) OpenURL(url *url.URL) error { + cmd := exec.Command("open", url.String()) + cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr + return cmd.Run() +} + +// SetSystemTrayIcon sets a custom image for the system tray icon. +// You should have previously called `SetSystemTrayMenu` to initialise the menu icon. +func (a *fyneApp) SetSystemTrayIcon(icon fyne.Resource) { + a.Driver().(systrayDriver).SetSystemTrayIcon(icon) +} + +// SetSystemTrayMenu creates a system tray item and attaches the specified menu. +// By default, this will use the application icon. +func (a *fyneApp) SetSystemTrayMenu(menu *fyne.Menu) { + if desk, ok := a.Driver().(systrayDriver); ok { + desk.SetSystemTrayMenu(menu) + } +} + +// SetSystemTrayWindow assigns a window to be shown with the system tray menu is tapped. +// You should have previously called `SetSystemTrayMenu` to initialise the menu icon. +func (a *fyneApp) SetSystemTrayWindow(w fyne.Window) { + a.Driver().(systrayDriver).SetSystemTrayWindow(w) +} + +//export themeChanged +func themeChanged() { + fyne.CurrentApp().Settings().(*settings).setupTheme() +} + +func watchTheme(_ *settings) { + C.watchTheme() +} + +func (a *fyneApp) registerRepositories() { + // no-op +} diff --git a/vendor/fyne.io/fyne/v2/app/app_desktop_darwin.m b/vendor/fyne.io/fyne/v2/app/app_desktop_darwin.m new file mode 100644 index 0000000..3067f3f --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/app_desktop_darwin.m @@ -0,0 +1,12 @@ +//go:build !ci && !ios && !wasm && !test_web_driver && !mobile + +extern void themeChanged(); + +#import + +void watchTheme() { + [[NSDistributedNotificationCenter defaultCenter] addObserverForName:@"AppleInterfaceThemeChangedNotification" object:nil queue:nil + usingBlock:^(NSNotification *note) { + themeChanged(); // calls back into Go + }]; +} diff --git a/vendor/fyne.io/fyne/v2/app/app_gl.go b/vendor/fyne.io/fyne/v2/app/app_gl.go new file mode 100644 index 0000000..052b3b9 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/app_gl.go @@ -0,0 +1,14 @@ +//go:build !ci && !android && !ios && !mobile && !tamago && !noos && !tinygo + +package app + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/driver/glfw" +) + +// NewWithID returns a new app instance using the appropriate runtime driver. +// The ID string should be globally unique to this app. +func NewWithID(id string) fyne.App { + return newAppWithDriver(glfw.NewGLDriver(), glfw.NewClipboard(), id) +} diff --git a/vendor/fyne.io/fyne/v2/app/app_mobile.go b/vendor/fyne.io/fyne/v2/app/app_mobile.go new file mode 100644 index 0000000..8d277c2 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/app_mobile.go @@ -0,0 +1,26 @@ +//go:build !ci && (android || ios || mobile) + +package app + +import ( + "fyne.io/fyne/v2" + internalapp "fyne.io/fyne/v2/internal/app" + "fyne.io/fyne/v2/internal/driver/mobile" +) + +// NewWithID returns a new app instance using the appropriate runtime driver. +// The ID string should be globally unique to this app. +func NewWithID(id string) fyne.App { + d := mobile.NewGoMobileDriver() + a := newAppWithDriver(d, mobile.NewClipboard(), id) + d.(mobile.ConfiguredDriver).SetOnConfigurationChanged(func(c *mobile.Configuration) { + internalapp.SystemTheme = c.SystemTheme + + a.Settings().(*settings).setupTheme() + }) + return a +} + +func (a *fyneApp) registerRepositories() { + // no-op +} diff --git a/vendor/fyne.io/fyne/v2/app/app_mobile_and.c b/vendor/fyne.io/fyne/v2/app/app_mobile_and.c new file mode 100644 index 0000000..a21e330 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/app_mobile_and.c @@ -0,0 +1,130 @@ +//go:build !ci && android + +#include +#include +#include +#include + +#define LOG_FATAL(...) __android_log_print(ANDROID_LOG_FATAL, "Fyne", __VA_ARGS__) + +static jclass find_class(JNIEnv *env, const char *class_name) { + jclass clazz = (*env)->FindClass(env, class_name); + if (clazz == NULL) { + (*env)->ExceptionClear(env); + LOG_FATAL("cannot find %s", class_name); + return NULL; + } + return clazz; +} + +static jmethodID find_method(JNIEnv *env, jclass clazz, const char *name, const char *sig) { + jmethodID m = (*env)->GetMethodID(env, clazz, name, sig); + if (m == 0) { + (*env)->ExceptionClear(env); + LOG_FATAL("cannot find method %s %s", name, sig); + return 0; + } + return m; +} + +static jmethodID find_static_method(JNIEnv *env, jclass clazz, const char *name, const char *sig) { + jmethodID m = (*env)->GetStaticMethodID(env, clazz, name, sig); + if (m == 0) { + (*env)->ExceptionClear(env); + LOG_FATAL("cannot find method %s %s", name, sig); + return 0; + } + return m; +} + +jobject getSystemService(uintptr_t jni_env, uintptr_t ctx, char *service) { + JNIEnv *env = (JNIEnv*)jni_env; + jstring serviceStr = (*env)->NewStringUTF(env, service); + + jclass ctxClass = (*env)->GetObjectClass(env, (jobject)ctx); + jmethodID getSystemService = find_method(env, ctxClass, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;"); + + return (jobject)(*env)->CallObjectMethod(env, (jobject)ctx, getSystemService, serviceStr); +} + +int nextId = 1; + +bool isOreoOrLater(JNIEnv *env) { + jclass versionClass = find_class(env, "android/os/Build$VERSION" ); + jfieldID sdkIntFieldID = (*env)->GetStaticFieldID(env, versionClass, "SDK_INT", "I" ); + int sdkVersion = (*env)->GetStaticIntField(env, versionClass, sdkIntFieldID ); + + return sdkVersion >= 26; // O = Oreo, will not be defined for older builds +} + +jobject parseURL(uintptr_t jni_env, uintptr_t ctx, char* uriCstr) { + JNIEnv *env = (JNIEnv*)jni_env; + + jstring uriStr = (*env)->NewStringUTF(env, uriCstr); + jclass uriClass = find_class(env, "android/net/Uri"); + jmethodID parse = find_static_method(env, uriClass, "parse", "(Ljava/lang/String;)Landroid/net/Uri;"); + + return (jobject)(*env)->CallStaticObjectMethod(env, uriClass, parse, uriStr); +} + +void openURL(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx, char *url) { + JNIEnv *env = (JNIEnv*)jni_env; + jobject uri = parseURL(jni_env, ctx, url); + + jclass intentClass = find_class(env, "android/content/Intent"); + jfieldID viewFieldID = (*env)->GetStaticFieldID(env, intentClass, "ACTION_VIEW", "Ljava/lang/String;" ); + jstring view = (*env)->GetStaticObjectField(env, intentClass, viewFieldID); + + jmethodID constructor = find_method(env, intentClass, "", "(Ljava/lang/String;Landroid/net/Uri;)V"); + jobject intent = (*env)->NewObject(env, intentClass, constructor, view, uri); + + jclass contextClass = find_class(env, "android/content/Context"); + jmethodID start = find_method(env, contextClass, "startActivity", "(Landroid/content/Intent;)V"); + (*env)->CallVoidMethod(env, (jobject)ctx, start, intent); +} + +void sendNotification(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx, char *title, char *body) { + JNIEnv *env = (JNIEnv*)jni_env; + jstring titleStr = (*env)->NewStringUTF(env, title); + jstring bodyStr = (*env)->NewStringUTF(env, body); + + jclass cls = find_class(env, "android/app/Notification$Builder"); + jmethodID constructor = find_method(env, cls, "", "(Landroid/content/Context;)V"); + jobject builder = (*env)->NewObject(env, cls, constructor, ctx); + + jclass mgrCls = find_class(env, "android/app/NotificationManager"); + jobject mgr = getSystemService((uintptr_t)env, ctx, "notification"); + + if (isOreoOrLater(env)) { + jstring channelId = (*env)->NewStringUTF(env, "fyne-notif"); + jstring name = (*env)->NewStringUTF(env, "Fyne Notification"); + int importance = 4; // IMPORTANCE_HIGH + + jclass chanCls = find_class(env, "android/app/NotificationChannel"); + jmethodID constructor = find_method(env, chanCls, "", "(Ljava/lang/String;Ljava/lang/CharSequence;I)V"); + jobject channel = (*env)->NewObject(env, chanCls, constructor, channelId, name, importance); + + jmethodID createChannel = find_method(env, mgrCls, "createNotificationChannel", "(Landroid/app/NotificationChannel;)V"); + (*env)->CallVoidMethod(env, mgr, createChannel, channel); + + jmethodID setChannelId = find_method(env, cls, "setChannelId", "(Ljava/lang/String;)Landroid/app/Notification$Builder;"); + (*env)->CallObjectMethod(env, builder, setChannelId, channelId); + } + + jmethodID setContentTitle = find_method(env, cls, "setContentTitle", "(Ljava/lang/CharSequence;)Landroid/app/Notification$Builder;"); + (*env)->CallObjectMethod(env, builder, setContentTitle, titleStr); + + jmethodID setContentText = find_method(env, cls, "setContentText", "(Ljava/lang/CharSequence;)Landroid/app/Notification$Builder;"); + (*env)->CallObjectMethod(env, builder, setContentText, bodyStr); + + int iconID = 17629184; // constant of "unknown app icon" + jmethodID setSmallIcon = find_method(env, cls, "setSmallIcon", "(I)Landroid/app/Notification$Builder;"); + (*env)->CallObjectMethod(env, builder, setSmallIcon, iconID); + + jmethodID build = find_method(env, cls, "build", "()Landroid/app/Notification;"); + jobject notif = (*env)->CallObjectMethod(env, builder, build); + + jmethodID notify = find_method(env, mgrCls, "notify", "(ILandroid/app/Notification;)V"); + (*env)->CallVoidMethod(env, mgr, notify, nextId, notif); + nextId++; +} diff --git a/vendor/fyne.io/fyne/v2/app/app_mobile_and.go b/vendor/fyne.io/fyne/v2/app/app_mobile_and.go new file mode 100644 index 0000000..3b03e72 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/app_mobile_and.go @@ -0,0 +1,44 @@ +//go:build !ci && android + +package app + +/* +#cgo LDFLAGS: -landroid -llog + +#include + +void openURL(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx, char *url); +void sendNotification(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx, char *title, char *content); +*/ +import "C" + +import ( + "net/url" + "unsafe" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/driver/mobile/app" +) + +func (a *fyneApp) OpenURL(url *url.URL) error { + urlStr := C.CString(url.String()) + defer C.free(unsafe.Pointer(urlStr)) + + app.RunOnJVM(func(vm, env, ctx uintptr) error { + C.openURL(C.uintptr_t(vm), C.uintptr_t(env), C.uintptr_t(ctx), urlStr) + return nil + }) + return nil +} + +func (a *fyneApp) SendNotification(n *fyne.Notification) { + titleStr := C.CString(n.Title) + defer C.free(unsafe.Pointer(titleStr)) + contentStr := C.CString(n.Content) + defer C.free(unsafe.Pointer(contentStr)) + + app.RunOnJVM(func(vm, env, ctx uintptr) error { + C.sendNotification(C.uintptr_t(vm), C.uintptr_t(env), C.uintptr_t(ctx), titleStr, contentStr) + return nil + }) +} diff --git a/vendor/fyne.io/fyne/v2/app/app_mobile_ios.go b/vendor/fyne.io/fyne/v2/app/app_mobile_ios.go new file mode 100644 index 0000000..51ac509 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/app_mobile_ios.go @@ -0,0 +1,27 @@ +//go:build !ci && ios && !mobile + +package app + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Foundation -framework UIKit -framework UserNotifications + +#include + +void openURL(char *urlStr); +void sendNotification(char *title, char *content); +*/ +import "C" + +import ( + "net/url" + "unsafe" +) + +func (a *fyneApp) OpenURL(url *url.URL) error { + urlStr := C.CString(url.String()) + C.openURL(urlStr) + C.free(unsafe.Pointer(urlStr)) + + return nil +} diff --git a/vendor/fyne.io/fyne/v2/app/app_mobile_ios.m b/vendor/fyne.io/fyne/v2/app/app_mobile_ios.m new file mode 100644 index 0000000..bfdbfeb --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/app_mobile_ios.m @@ -0,0 +1,10 @@ +//go:build !ci && ios + +#import + +void openURL(char *urlStr) { + UIApplication *app = [UIApplication sharedApplication]; + NSURL *url = [NSURL URLWithString:[NSString stringWithUTF8String:urlStr]]; + [app openURL:url options:@{} completionHandler:nil]; +} + diff --git a/vendor/fyne.io/fyne/v2/app/app_mobile_xdg.go b/vendor/fyne.io/fyne/v2/app/app_mobile_xdg.go new file mode 100644 index 0000000..232e1d9 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/app_mobile_xdg.go @@ -0,0 +1,22 @@ +//go:build !ci && mobile && !android && !ios + +package app + +import ( + "errors" + "net/url" + + "fyne.io/fyne/v2" +) + +func (a *fyneApp) OpenURL(_ *url.URL) error { + return errors.New("mobile simulator does not support open URLs yet") +} + +func (a *fyneApp) SendNotification(_ *fyne.Notification) { + fyne.LogError("Notifications are not supported in the mobile simulator yet", nil) +} + +func watchTheme(_ *settings) { + // not implemented yet +} diff --git a/vendor/fyne.io/fyne/v2/app/app_noos.go b/vendor/fyne.io/fyne/v2/app/app_noos.go new file mode 100644 index 0000000..0af08ca --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/app_noos.go @@ -0,0 +1,23 @@ +//go:build tamago || noos || tinygo + +package app + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/driver/embedded" + intNoos "fyne.io/fyne/v2/internal/driver/embedded" + "fyne.io/fyne/v2/theme" +) + +func NewWithID(id string) fyne.App { + return newAppWithDriver(nil, nil, id) +} + +// SetDriverDetails provides the required information to our app to run without a standard +// driver. This is useful for embedded devices like GOOS=tamago, tinygo or noos. +// +// Since: 2.7 +func SetDriverDetails(a fyne.App, d embedded.Driver) { + a.(*fyneApp).Settings().SetTheme(theme.DefaultTheme()) + a.(*fyneApp).driver = intNoos.NewNoOSDriver(d.Render, d.Run, d.Queue(), d.ScreenSize) +} diff --git a/vendor/fyne.io/fyne/v2/app/app_notlegacy_darwin.go b/vendor/fyne.io/fyne/v2/app/app_notlegacy_darwin.go new file mode 100644 index 0000000..f1c065e --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/app_notlegacy_darwin.go @@ -0,0 +1,8 @@ +//go:build !ci && !legacy && !wasm && !test_web_driver + +package app + +/* +#cgo LDFLAGS: -framework Foundation -framework UserNotifications +*/ +import "C" diff --git a/vendor/fyne.io/fyne/v2/app/app_openurl_wasm.go b/vendor/fyne.io/fyne/v2/app/app_openurl_wasm.go new file mode 100644 index 0000000..c92224d --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/app_openurl_wasm.go @@ -0,0 +1,18 @@ +//go:build !ci && wasm + +package app + +import ( + "fmt" + "net/url" + "syscall/js" +) + +func (a *fyneApp) OpenURL(url *url.URL) error { + window := js.Global().Call("open", url.String(), "_blank", "") + if window.Equal(js.Null()) { + return fmt.Errorf("Unable to open a new window/tab for URL: %v.", url) + } + window.Call("focus") + return nil +} diff --git a/vendor/fyne.io/fyne/v2/app/app_openurl_web.go b/vendor/fyne.io/fyne/v2/app/app_openurl_web.go new file mode 100644 index 0000000..29b5a18 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/app_openurl_web.go @@ -0,0 +1,12 @@ +//go:build !ci && !wasm && test_web_driver + +package app + +import ( + "errors" + "net/url" +) + +func (a *fyneApp) OpenURL(url *url.URL) error { + return errors.New("OpenURL is not supported with the test web driver.") +} diff --git a/vendor/fyne.io/fyne/v2/app/app_other.go b/vendor/fyne.io/fyne/v2/app/app_other.go new file mode 100644 index 0000000..645720e --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/app_other.go @@ -0,0 +1,26 @@ +//go:build ci || (!ios && !android && !linux && !darwin && !windows && !freebsd && !openbsd && !netbsd && !wasm && !test_web_driver) || tamago || noos || tinygo + +package app + +import ( + "errors" + "net/url" + + "fyne.io/fyne/v2" +) + +func (a *fyneApp) OpenURL(_ *url.URL) error { + return errors.New("unable to open url for unknown operating system") +} + +func (a *fyneApp) SendNotification(_ *fyne.Notification) { + fyne.LogError("Refusing to show notification for unknown operating system", nil) +} + +func watchTheme(_ *settings) { + // no-op +} + +func (a *fyneApp) registerRepositories() { + // no-op +} diff --git a/vendor/fyne.io/fyne/v2/app/app_software.go b/vendor/fyne.io/fyne/v2/app/app_software.go new file mode 100644 index 0000000..2821ce9 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/app_software.go @@ -0,0 +1,15 @@ +//go:build ci + +package app + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/painter/software" + "fyne.io/fyne/v2/test" +) + +// NewWithID returns a new app instance using the test (headless) driver. +// The ID string should be globally unique to this app. +func NewWithID(id string) fyne.App { + return newAppWithDriver(test.NewDriverWithPainter(software.NewPainter()), test.NewClipboard(), id) +} diff --git a/vendor/fyne.io/fyne/v2/app/app_wasm.go b/vendor/fyne.io/fyne/v2/app/app_wasm.go new file mode 100644 index 0000000..5c3ac54 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/app_wasm.go @@ -0,0 +1,75 @@ +//go:build !ci && (!android || !ios || !mobile) && (wasm || test_web_driver) + +package app + +import ( + "encoding/base64" + "fmt" + "net/http" + "syscall/js" + + "fyne.io/fyne/v2" + intRepo "fyne.io/fyne/v2/internal/repository" + "fyne.io/fyne/v2/storage/repository" +) + +func (a *fyneApp) SendNotification(n *fyne.Notification) { + notification := js.Global().Get("window").Get("Notification") + if notification.IsUndefined() { + fyne.LogError("Current browser does not support notifications.", nil) + return + } + + permission := notification.Get("permission") + if permission.Type() != js.TypeString || permission.String() != "granted" { + request := js.FuncOf(func(this js.Value, args []js.Value) any { + if len(args) > 0 && args[0].Type() == js.TypeString && args[0].String() == "granted" { + a.showNotification(n, ¬ification) + } else { + fyne.LogError("User rejected the request for notifications.", nil) + } + return nil + }) + defer request.Release() + notification.Call("requestPermission", request) + return + } + + a.showNotification(n, ¬ification) +} + +func (a *fyneApp) showNotification(data *fyne.Notification, notification *js.Value) { + icon := a.icon.Content() + base64Str := base64.StdEncoding.EncodeToString(icon) + mimeType := http.DetectContentType(icon) + base64Img := fmt.Sprintf("data:%s;base64,%s", mimeType, base64Str) + notification.New(data.Title, map[string]any{ + "body": data.Content, + "icon": base64Img, + }) +} + +var themeChanged = js.FuncOf(func(this js.Value, args []js.Value) any { + if len(args) > 0 && args[0].Type() == js.TypeObject { + fyne.CurrentApp().Settings().(*settings).setupTheme() + } + return nil +}) + +func watchTheme(_ *settings) { + js.Global().Call("matchMedia", "(prefers-color-scheme: dark)").Call("addEventListener", "change", themeChanged) +} + +func stopWatchingTheme() { + js.Global().Call("matchMedia", "(prefers-color-scheme: dark)").Call("removeEventListener", "change", themeChanged) +} + +func (a *fyneApp) registerRepositories() { + repo, err := intRepo.NewIndexDBRepository() + if err != nil { + fyne.LogError("failed to create repository: %v", err) + return + } + + repository.Register("idbfile", repo) +} diff --git a/vendor/fyne.io/fyne/v2/app/app_windows.go b/vendor/fyne.io/fyne/v2/app/app_windows.go new file mode 100644 index 0000000..cc12e36 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/app_windows.go @@ -0,0 +1,106 @@ +//go:build !ci && !android && !ios && !wasm && !test_web_driver && !tinygo + +package app + +import ( + "fmt" + "net/url" + "os" + "os/exec" + "path/filepath" + "strings" + "syscall" + + "fyne.io/fyne/v2" + internalapp "fyne.io/fyne/v2/internal/app" +) + +const notificationTemplate = `$title = "%s" +$content = "%s" +$iconPath = "file:///%s" +[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null +$template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastImageAndText02) +$toastXml = [xml] $template.GetXml() +$toastXml.GetElementsByTagName("text")[0].AppendChild($toastXml.CreateTextNode($title)) > $null +$toastXml.GetElementsByTagName("text")[1].AppendChild($toastXml.CreateTextNode($content)) > $null +$toastXml.GetElementsByTagName("image")[0].SetAttribute("src", $iconPath) > $null +$xml = New-Object Windows.Data.Xml.Dom.XmlDocument +$xml.LoadXml($toastXml.OuterXml) +$toast = [Windows.UI.Notifications.ToastNotification]::new($xml) +[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("%s").Show($toast);` + +func (a *fyneApp) OpenURL(url *url.URL) error { + cmd := exec.Command("rundll32", "url.dll,FileProtocolHandler", url.String()) + cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr + return cmd.Run() +} + +var scriptNum = 0 + +func (a *fyneApp) SendNotification(n *fyne.Notification) { + title := escapeNotificationString(n.Title) + content := escapeNotificationString(n.Content) + iconFilePath := a.cachedIconPath() + appID := a.UniqueID() + if appID == "" || strings.Index(appID, "missing-id") == 0 { + appID = a.Metadata().Name + } + + script := fmt.Sprintf(notificationTemplate, title, content, iconFilePath, appID) + go runScript("notify", script) +} + +// SetSystemTrayMenu creates a system tray item and attaches the specified menu. +// By default, this will use the application icon. +func (a *fyneApp) SetSystemTrayMenu(menu *fyne.Menu) { + a.Driver().(systrayDriver).SetSystemTrayMenu(menu) +} + +// SetSystemTrayIcon sets a custom image for the system tray icon. +// You should have previously called `SetSystemTrayMenu` to initialise the menu icon. +func (a *fyneApp) SetSystemTrayIcon(icon fyne.Resource) { + a.Driver().(systrayDriver).SetSystemTrayIcon(icon) +} + +// SetSystemTrayWindow assigns a window to be shown with the system tray menu is tapped. +// You should have previously called `SetSystemTrayMenu` to initialise the menu icon. +func (a *fyneApp) SetSystemTrayWindow(w fyne.Window) { + a.Driver().(systrayDriver).SetSystemTrayWindow(w) +} + +func escapeNotificationString(in string) string { + noSlash := strings.ReplaceAll(in, "`", "``") + return strings.ReplaceAll(noSlash, "\"", "`\"") +} + +func runScript(name, script string) { + scriptNum++ + appID := fyne.CurrentApp().UniqueID() + fileName := fmt.Sprintf("fyne-%s-%s-%d.ps1", appID, name, scriptNum) + + tmpFilePath := filepath.Join(os.TempDir(), fileName) + err := os.WriteFile(tmpFilePath, []byte(script), 0o600) + if err != nil { + fyne.LogError("Could not write script to show notification", err) + return + } + defer os.Remove(tmpFilePath) + + launch := "(Get-Content -Encoding UTF8 -Path " + tmpFilePath + " -Raw) | Invoke-Expression" + cmd := exec.Command("PowerShell", "-ExecutionPolicy", "Bypass", launch) + cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} + err = cmd.Run() + if err != nil { + fyne.LogError("Failed to launch windows notify script", err) + } +} + +func watchTheme(s *settings) { + go internalapp.WatchTheme(func() { + fyne.Do(s.setupTheme) + }) +} + +func (a *fyneApp) registerRepositories() { + // no-op +} diff --git a/vendor/fyne.io/fyne/v2/app/app_xdg.go b/vendor/fyne.io/fyne/v2/app/app_xdg.go new file mode 100644 index 0000000..edc1f2b --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/app_xdg.go @@ -0,0 +1,150 @@ +//go:build !ci && !wasm && !test_web_driver && !android && !ios && !mobile && (linux || openbsd || freebsd || netbsd) && !tinygo && !noos && !tamago + +package app + +import ( + "net/url" + "os" + "os/exec" + "sync/atomic" + + "github.com/godbus/dbus/v5" + "github.com/rymdport/portal/notification" + "github.com/rymdport/portal/openuri" + portalSettings "github.com/rymdport/portal/settings" + "github.com/rymdport/portal/settings/appearance" + + "fyne.io/fyne/v2" + internalapp "fyne.io/fyne/v2/internal/app" + "fyne.io/fyne/v2/internal/build" + "fyne.io/fyne/v2/theme" +) + +const systemTheme = fyne.ThemeVariant(99) + +func (a *fyneApp) OpenURL(url *url.URL) error { + if build.IsFlatpak { + err := openuri.OpenURI("", url.String(), nil) + if err != nil { + fyne.LogError("Opening url in portal failed", err) + } + return err + } + + cmd := exec.Command("xdg-open", url.String()) + cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr + return cmd.Start() +} + +// fetch color variant from dbus portal desktop settings. +func findFreedesktopColorScheme() fyne.ThemeVariant { + colorScheme, err := appearance.GetColorScheme() + if err != nil { + return systemTheme + } + + return colorSchemeToThemeVariant(colorScheme) +} + +func colorSchemeToThemeVariant(colorScheme appearance.ColorScheme) fyne.ThemeVariant { + switch colorScheme { + case appearance.Light: + return theme.VariantLight + case appearance.Dark: + return theme.VariantDark + } + + // Default to light theme to support Gnome's default see https://github.com/fyne-io/fyne/pull/3561 + return theme.VariantLight +} + +func (a *fyneApp) SendNotification(n *fyne.Notification) { + if build.IsFlatpak { + err := a.sendNotificationThroughPortal(n) + if err != nil { + fyne.LogError("Sending notification using portal failed", err) + } + return + } + + conn, err := dbus.SessionBus() // shared connection, don't close + if err != nil { + fyne.LogError("Unable to connect to session D-Bus", err) + return + } + + appIcon := a.cachedIconPath() + timeout := int32(0) // we don't support this yet + + obj := conn.Object("org.freedesktop.Notifications", "/org/freedesktop/Notifications") + call := obj.Call("org.freedesktop.Notifications.Notify", 0, a.uniqueID, uint32(0), + appIcon, n.Title, n.Content, []string{}, map[string]dbus.Variant{}, timeout) + if call.Err != nil { + fyne.LogError("Failed to send message to bus", call.Err) + } +} + +// Sending with same ID replaces the old notification. +var notificationID atomic.Uint64 + +// See https://flatpak.github.io/xdg-desktop-portal/docs/#gdbus-org.freedesktop.portal.Notification. +func (a *fyneApp) sendNotificationThroughPortal(n *fyne.Notification) error { + return notification.Add( + uint(notificationID.Add(1)), + notification.Content{ + Title: n.Title, + Body: n.Content, + Icon: a.uniqueID, + }, + ) +} + +// SetSystemTrayMenu creates a system tray item and attaches the specified menu. +// By default, this will use the application icon. +func (a *fyneApp) SetSystemTrayMenu(menu *fyne.Menu) { + if desk, ok := a.Driver().(systrayDriver); ok { // don't use this on mobile tag + desk.SetSystemTrayMenu(menu) + } +} + +// SetSystemTrayIcon sets a custom image for the system tray icon. +// You should have previously called `SetSystemTrayMenu` to initialise the menu icon. +func (a *fyneApp) SetSystemTrayIcon(icon fyne.Resource) { + if desk, ok := a.Driver().(systrayDriver); ok { // don't use this on mobile tag + desk.SetSystemTrayIcon(icon) + } +} + +// SetSystemTrayWindow assigns a window to be shown with the system tray menu is tapped. +// You should have previously called `SetSystemTrayMenu` to initialise the menu icon. +func (a *fyneApp) SetSystemTrayWindow(w fyne.Window) { + a.Driver().(systrayDriver).SetSystemTrayWindow(w) +} + +func watchTheme(s *settings) { + go func() { + // Theme lookup hangs on some desktops. Update theme variant cache from within goroutine. + themeVariant := findFreedesktopColorScheme() + if themeVariant != systemTheme { + internalapp.CurrentVariant.Store(uint64(themeVariant)) + fyne.Do(func() { s.applyVariant(themeVariant) }) + } + + portalSettings.OnSignalSettingChanged(func(changed portalSettings.Changed) { + if changed.Namespace == appearance.Namespace && changed.Key == "color-scheme" { + themeVariant := colorSchemeToThemeVariant(appearance.ColorScheme(changed.Value.(uint32))) + internalapp.CurrentVariant.Store(uint64(themeVariant)) + fyne.Do(func() { s.applyVariant(themeVariant) }) + } + }) + }() +} + +func (a *fyneApp) registerRepositories() { + // no-op +} + +func (s *settings) applyVariant(variant fyne.ThemeVariant) { + s.variant = variant + s.apply() +} diff --git a/vendor/fyne.io/fyne/v2/app/cloud.go b/vendor/fyne.io/fyne/v2/app/cloud.go new file mode 100644 index 0000000..22c43cf --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/cloud.go @@ -0,0 +1,47 @@ +package app + +import "fyne.io/fyne/v2" + +func (a *fyneApp) SetCloudProvider(p fyne.CloudProvider) { + if p == nil { + a.cloud = nil + return + } + + a.transitionCloud(p) +} + +func (a *fyneApp) transitionCloud(p fyne.CloudProvider) { + if a.cloud != nil { + a.cloud.Cleanup(a) + } + + err := p.Setup(a) + if err != nil { + fyne.LogError("Failed to set up cloud provider "+p.ProviderName(), err) + return + } + a.cloud = p + + listeners := a.prefs.ChangeListeners() + if pp, ok := p.(fyne.CloudProviderPreferences); ok { + a.prefs = pp.CloudPreferences(a) + } else { + a.prefs = a.newDefaultPreferences() + } + if cloud, ok := p.(fyne.CloudProviderStorage); ok { + a.storage = cloud.CloudStorage(a) + } else { + store := &store{a: a} + store.Docs = makeStoreDocs(a.uniqueID, store) + a.storage = store + } + + for _, l := range listeners { + a.prefs.AddChangeListener(l) + l() // assume that preferences have changed because we replaced the provider + } + + // after transition ensure settings listener is fired + a.settings.apply() +} diff --git a/vendor/fyne.io/fyne/v2/app/icon_cache_file.go b/vendor/fyne.io/fyne/v2/app/icon_cache_file.go new file mode 100644 index 0000000..3245dec --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/icon_cache_file.go @@ -0,0 +1,54 @@ +package app + +import ( + "os" + "path/filepath" + "sync" + + "fyne.io/fyne/v2" +) + +var once sync.Once + +func (a *fyneApp) cachedIconPath() string { + if a.Icon() == nil { + return "" + } + + dirPath := filepath.Join(rootCacheDir(), a.UniqueID()) + filePath := filepath.Join(dirPath, "icon.png") + once.Do(func() { + err := a.saveIconToCache(dirPath, filePath) + if err != nil { + filePath = "" + } + }) + + return filePath +} + +func (a *fyneApp) saveIconToCache(dirPath, filePath string) error { + err := os.MkdirAll(dirPath, 0o700) + if err != nil { + fyne.LogError("Unable to create application cache directory", err) + return err + } + + file, err := os.Create(filePath) + if err != nil { + fyne.LogError("Unable to create icon file", err) + return err + } + + defer file.Close() + + if icon := a.Icon(); icon != nil { + _, err = file.Write(icon.Content()) + if err != nil { + fyne.LogError("Unable to write icon contents", err) + return err + } + } + + return nil +} diff --git a/vendor/fyne.io/fyne/v2/app/icon_cache_noos.go b/vendor/fyne.io/fyne/v2/app/icon_cache_noos.go new file mode 100644 index 0000000..f837dc4 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/icon_cache_noos.go @@ -0,0 +1,13 @@ +//go:build noos || tinygo + +package app + +import ( + "os" + "path/filepath" +) + +func rootCacheDir() string { + home, _ := os.UserHomeDir() + return filepath.Join(home, ".config", "fyne") +} diff --git a/vendor/fyne.io/fyne/v2/app/icon_cache_other.go b/vendor/fyne.io/fyne/v2/app/icon_cache_other.go new file mode 100644 index 0000000..53773d8 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/icon_cache_other.go @@ -0,0 +1,13 @@ +//go:build !noos && !tinygo + +package app + +import ( + "os" + "path/filepath" +) + +func rootCacheDir() string { + desktopCache, _ := os.UserCacheDir() + return filepath.Join(desktopCache, "fyne") +} diff --git a/vendor/fyne.io/fyne/v2/app/meta.go b/vendor/fyne.io/fyne/v2/app/meta.go new file mode 100644 index 0000000..214f13b --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/meta.go @@ -0,0 +1,36 @@ +package app + +import ( + "fyne.io/fyne/v2" +) + +var meta = fyne.AppMetadata{ + ID: "", + Name: "", + Version: "0.0.1", + Build: 1, + Release: false, + Custom: map[string]string{}, + Migrations: map[string]bool{}, +} + +// SetMetadata overrides the packaged application metadata. +// This data can be used in many places like notifications and about screens. +func SetMetadata(m fyne.AppMetadata) { + meta = m + + if meta.Custom == nil { + meta.Custom = map[string]string{} + } + if meta.Migrations == nil { + meta.Migrations = map[string]bool{} + } +} + +func (a *fyneApp) Metadata() fyne.AppMetadata { + if meta.ID == "" && meta.Name == "" { + checkLocalMetadata() + } + + return meta +} diff --git a/vendor/fyne.io/fyne/v2/app/meta_development.go b/vendor/fyne.io/fyne/v2/app/meta_development.go new file mode 100644 index 0000000..0c29a7c --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/meta_development.go @@ -0,0 +1,65 @@ +package app + +import ( + "os" + "path/filepath" + "strings" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/build" + "fyne.io/fyne/v2/internal/metadata" +) + +func checkLocalMetadata() { + if build.NoMetadata || build.Mode == fyne.BuildRelease { + return + } + + dir := getProjectPath() + file := filepath.Join(dir, "FyneApp.toml") + ref, err := os.Open(file) + if err != nil { // no worries, this is just an optional fallback + return + } + defer ref.Close() + + data, err := metadata.Load(ref) + if err != nil || data == nil { + fyne.LogError("failed to parse FyneApp.toml", err) + return + } + + meta.ID = data.Details.ID + meta.Name = data.Details.Name + meta.Version = data.Details.Version + meta.Build = data.Details.Build + + if data.Details.Icon != "" { + res, err := fyne.LoadResourceFromPath(data.Details.Icon) + if err == nil { + meta.Icon = metadata.ScaleIcon(res, 512) + } + } + + meta.Release = false + meta.Custom = data.Development + meta.Migrations = data.Migrations +} + +func getProjectPath() string { + exe, err := os.Executable() + work, _ := os.Getwd() + + if err != nil { + fyne.LogError("failed to lookup build executable", err) + return work + } + + temp := os.TempDir() + if strings.Contains(exe, temp) || strings.Contains(exe, "go-build") { // this happens with "go run" + return work + } + + // we were called with an executable from "go build" + return filepath.Dir(exe) +} diff --git a/vendor/fyne.io/fyne/v2/app/preferences.go b/vendor/fyne.io/fyne/v2/app/preferences.go new file mode 100644 index 0000000..75ba039 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/preferences.go @@ -0,0 +1,191 @@ +package app + +import ( + "encoding/json" + "errors" + "io" + "sync" + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal" +) + +type preferences struct { + *internal.InMemoryPreferences + + prefLock sync.RWMutex + savedRecently bool + changedDuringSaving bool + + app *fyneApp + needsSaveBeforeExit bool +} + +// Declare conformity with Preferences interface +var _ fyne.Preferences = (*preferences)(nil) + +// sentinel error to signal an empty preferences storage backend was loaded +var errEmptyPreferencesStore = errors.New("empty preferences store") + +// returned from storageWriter() - may be a file, browser local storage, etc +type writeSyncCloser interface { + io.WriteCloser + Sync() error +} + +// forceImmediateSave writes preferences to storage immediately, ignoring the debouncing +// logic in the change listener. Does nothing if preferences are not backed with a persistent store. +func (p *preferences) forceImmediateSave() { + if !p.needsSaveBeforeExit { + return + } + err := p.save() + if err != nil { + fyne.LogError("Failed on force saving preferences", err) + } +} + +func (p *preferences) resetSavedRecently() { + go func() { + time.Sleep(time.Millisecond * 100) // writes are not always atomic. 10ms worked, 100 is safer. + + // For test reasons we need to use current app not what we were initialised with as they can differ + fyne.DoAndWait(func() { + p.prefLock.Lock() + p.savedRecently = false + changedDuringSaving := p.changedDuringSaving + p.changedDuringSaving = false + p.prefLock.Unlock() + + if changedDuringSaving { + p.save() + } + }) + }() +} + +func (p *preferences) save() error { + storage, err := p.storageWriter() + if err != nil { + return err + } + return p.saveToStorage(storage) +} + +func (p *preferences) saveToStorage(writer writeSyncCloser) error { + p.prefLock.Lock() + p.savedRecently = true + p.prefLock.Unlock() + defer p.resetSavedRecently() + + defer writer.Close() + encode := json.NewEncoder(writer) + + var err error + p.InMemoryPreferences.ReadValues(func(values map[string]any) { + err = encode.Encode(&values) + }) + + err2 := writer.Sync() + if err == nil { + err = err2 + } + return err +} + +func (p *preferences) load() { + storage, err := p.storageReader() + if err == nil { + err = p.loadFromStorage(storage) + } + if err != nil && err != errEmptyPreferencesStore { + fyne.LogError("Preferences load error:", err) + } +} + +func (p *preferences) loadFromStorage(storage io.ReadCloser) (err error) { + defer func() { + if r := storage.Close(); r != nil && err == nil { + err = r + } + }() + decode := json.NewDecoder(storage) + + p.InMemoryPreferences.WriteValues(func(values map[string]any) { + err = decode.Decode(&values) + if err != nil { + return + } + convertLists(values) + }) + + return err +} + +func newPreferences(app *fyneApp) *preferences { + p := &preferences{} + p.app = app + p.InMemoryPreferences = internal.NewInMemoryPreferences() + + // don't load or watch if not setup + if app.uniqueID == "" && app.Metadata().ID == "" { + return p + } + + p.needsSaveBeforeExit = true + p.AddChangeListener(func() { + if p != app.prefs { + return + } + p.prefLock.Lock() + shouldIgnoreChange := p.savedRecently + if p.savedRecently { + p.changedDuringSaving = true + } + p.prefLock.Unlock() + + if shouldIgnoreChange { // callback after loading from storage, or too many updates in a row + return + } + + err := p.save() + if err != nil { + fyne.LogError("Failed on saving preferences", err) + } + }) + p.watch() + return p +} + +func convertLists(values map[string]any) { + for k, v := range values { + if items, ok := v.([]any); ok { + if len(items) == 0 { + continue + } + + switch items[0].(type) { + case bool: + bools := make([]bool, len(items)) + for i, item := range items { + bools[i] = item.(bool) + } + values[k] = bools + case float64: + floats := make([]float64, len(items)) + for i, item := range items { + floats[i] = item.(float64) + } + values[k] = floats + // case int: // json has no int! + case string: + strings := make([]string, len(items)) + for i, item := range items { + strings[i] = item.(string) + } + values[k] = strings + } + } + } +} diff --git a/vendor/fyne.io/fyne/v2/app/preferences_android.go b/vendor/fyne.io/fyne/v2/app/preferences_android.go new file mode 100644 index 0000000..ac9f834 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/preferences_android.go @@ -0,0 +1,24 @@ +//go:build android + +package app + +import ( + "path/filepath" + + "fyne.io/fyne/v2/internal/app" +) + +// storagePath returns the location of the settings storage +func (p *preferences) storagePath() string { + // we have no global storage, use app global instead - rootConfigDir looks up in app_mobile_and.go + return filepath.Join(p.app.storageRoot(), "preferences.json") +} + +// storageRoot returns the location of the app storage +func (a *fyneApp) storageRoot() string { + return app.RootConfigDir() // we are in a sandbox, so no app ID added to this path +} + +func (p *preferences) watch() { + // no-op on mobile +} diff --git a/vendor/fyne.io/fyne/v2/app/preferences_ios.go b/vendor/fyne.io/fyne/v2/app/preferences_ios.go new file mode 100644 index 0000000..b861eec --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/preferences_ios.go @@ -0,0 +1,25 @@ +//go:build ios + +package app + +import ( + "path/filepath" + + "fyne.io/fyne/v2/internal/app" +) +import "C" + +// storagePath returns the location of the settings storage +func (p *preferences) storagePath() string { + ret := filepath.Join(p.app.storageRoot(), "preferences.json") + return ret +} + +// storageRoot returns the location of the app storage +func (a *fyneApp) storageRoot() string { + return app.RootConfigDir() // we are in a sandbox, so no app ID added to this path +} + +func (p *preferences) watch() { + // no-op on mobile +} diff --git a/vendor/fyne.io/fyne/v2/app/preferences_mobile.go b/vendor/fyne.io/fyne/v2/app/preferences_mobile.go new file mode 100644 index 0000000..1fd186f --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/preferences_mobile.go @@ -0,0 +1,23 @@ +//go:build mobile + +package app + +import ( + "path/filepath" + + "fyne.io/fyne/v2/internal/app" +) + +// storagePath returns the location of the settings storage +func (p *preferences) storagePath() string { + return filepath.Join(p.app.storageRoot(), "preferences.json") +} + +// storageRoot returns the location of the app storage +func (a *fyneApp) storageRoot() string { + return filepath.Join(app.RootConfigDir(), a.UniqueID()) +} + +func (p *preferences) watch() { + // no-op as we are in mobile simulation mode +} diff --git a/vendor/fyne.io/fyne/v2/app/preferences_nonweb.go b/vendor/fyne.io/fyne/v2/app/preferences_nonweb.go new file mode 100644 index 0000000..74b7444 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/preferences_nonweb.go @@ -0,0 +1,67 @@ +//go:build !wasm + +package app + +import ( + "io" + "os" + "path/filepath" +) + +func (p *preferences) storageWriter() (writeSyncCloser, error) { + return p.storageWriterForPath(p.storagePath()) +} + +func (p *preferences) storageReader() (io.ReadCloser, error) { + return p.storageReaderForPath(p.storagePath()) +} + +func (p *preferences) storageWriterForPath(path string) (writeSyncCloser, error) { + err := os.MkdirAll(filepath.Dir(path), 0o700) + if err != nil { // this is not an exists error according to docs + return nil, err + } + file, err := os.Create(path) + if err != nil { + if !os.IsExist(err) { + return nil, err + } + file, err = os.Open(path) // #nosec + if err != nil { + return nil, err + } + } + return file, nil +} + +func (p *preferences) storageReaderForPath(path string) (io.ReadCloser, error) { + file, err := os.Open(path) // #nosec + if err != nil { + if os.IsNotExist(err) { + if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { + return nil, err + } + return nil, errEmptyPreferencesStore + } + return nil, err + } + return file, nil +} + +// the following are only used in tests to save preferences to a tmp file + +func (p *preferences) saveToFile(path string) error { + file, err := p.storageWriterForPath(path) + if err != nil { + return err + } + return p.saveToStorage(file) +} + +func (p *preferences) loadFromFile(path string) error { + file, err := p.storageReaderForPath(path) + if err != nil { + return err + } + return p.loadFromStorage(file) +} diff --git a/vendor/fyne.io/fyne/v2/app/preferences_other.go b/vendor/fyne.io/fyne/v2/app/preferences_other.go new file mode 100644 index 0000000..3e698c2 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/preferences_other.go @@ -0,0 +1,32 @@ +//go:build !ios && !android && !mobile && !wasm + +package app + +import ( + "path/filepath" + + "fyne.io/fyne/v2/internal/app" +) + +// storagePath returns the location of the settings storage +func (p *preferences) storagePath() string { + return filepath.Join(p.app.storageRoot(), "preferences.json") +} + +// storageRoot returns the location of the app storage +func (a *fyneApp) storageRoot() string { + return filepath.Join(app.RootConfigDir(), a.UniqueID()) +} + +func (p *preferences) watch() { + watchFile(p.storagePath(), func() { + p.prefLock.RLock() + shouldIgnoreChange := p.savedRecently + p.prefLock.RUnlock() + if shouldIgnoreChange { + return + } + + p.load() + }) +} diff --git a/vendor/fyne.io/fyne/v2/app/preferences_wasm.go b/vendor/fyne.io/fyne/v2/app/preferences_wasm.go new file mode 100644 index 0000000..7493f1d --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/preferences_wasm.go @@ -0,0 +1,62 @@ +//go:build wasm + +package app + +import ( + "bytes" + "io" + "strings" + "syscall/js" +) + +const preferencesLocalStorageKey = "fyne-preferences.json" + +func (a *fyneApp) storageRoot() string { + return "idbfile:///fyne/" +} + +func (p *preferences) storageReader() (io.ReadCloser, error) { + key := js.ValueOf(preferencesLocalStorageKey) + data := js.Global().Get("localStorage").Call("getItem", key) + if data.IsNull() || data.IsUndefined() { + return nil, errEmptyPreferencesStore + } + + return readerNopCloser{reader: strings.NewReader(data.String())}, nil +} + +func (p *preferences) storageWriter() (writeSyncCloser, error) { + return &localStorageWriter{key: preferencesLocalStorageKey}, nil +} + +func (p *preferences) watch() { + // no-op for web driver +} + +type readerNopCloser struct { + reader io.Reader +} + +func (r readerNopCloser) Read(b []byte) (int, error) { + return r.reader.Read(b) +} + +func (r readerNopCloser) Close() error { + return nil +} + +type localStorageWriter struct { + bytes.Buffer + key string +} + +func (s *localStorageWriter) Sync() error { + text := s.String() + s.Reset() + js.Global().Get("localStorage").Call("setItem", js.ValueOf(s.key), js.ValueOf(text)) + return nil +} + +func (s *localStorageWriter) Close() error { + return nil +} diff --git a/vendor/fyne.io/fyne/v2/app/settings.go b/vendor/fyne.io/fyne/v2/app/settings.go new file mode 100644 index 0000000..35ff8ca --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/settings.go @@ -0,0 +1,151 @@ +package app + +import ( + "os" + "path/filepath" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/app" + "fyne.io/fyne/v2/internal/async" + "fyne.io/fyne/v2/internal/build" + "fyne.io/fyne/v2/theme" +) + +// SettingsSchema is used for loading and storing global settings +type SettingsSchema struct { + // these items are used for global settings load + ThemeName string `json:"theme"` + Scale float32 `json:"scale"` + PrimaryColor string `json:"primary_color"` + CloudName string `json:"cloud_name"` + CloudConfig string `json:"cloud_config"` + DisableAnimations bool `json:"no_animations"` +} + +// StoragePath returns the location of the settings storage +func (sc *SettingsSchema) StoragePath() string { + return filepath.Join(app.RootConfigDir(), "settings.json") +} + +// Declare conformity with Settings interface +var _ fyne.Settings = (*settings)(nil) + +type settings struct { + theme fyne.Theme + themeSpecified bool + variant fyne.ThemeVariant + + listeners []func(fyne.Settings) + changeListeners async.Map[chan fyne.Settings, bool] + watcher any // normally *fsnotify.Watcher or nil - avoid import in this file + + schema SettingsSchema +} + +func (s *settings) BuildType() fyne.BuildType { + return build.Mode +} + +func (s *settings) PrimaryColor() string { + return s.schema.PrimaryColor +} + +// OverrideTheme allows the settings app to temporarily preview different theme details. +// Please make sure that you remember the original settings and call this again to revert the change. +// +// Deprecated: Use container.NewThemeOverride to change the appearance of part of your application. +func (s *settings) OverrideTheme(theme fyne.Theme, name string) { + s.schema.PrimaryColor = name + s.theme = theme +} + +func (s *settings) Theme() fyne.Theme { + if s == nil { + fyne.LogError("Attempt to access current Fyne theme when no app is started", nil) + return nil + } + return s.theme +} + +func (s *settings) SetTheme(theme fyne.Theme) { + s.themeSpecified = true + s.applyTheme(theme, s.variant) +} + +func (s *settings) ShowAnimations() bool { + return !s.schema.DisableAnimations && !build.NoAnimations +} + +func (s *settings) ThemeVariant() fyne.ThemeVariant { + return s.variant +} + +func (s *settings) applyTheme(theme fyne.Theme, variant fyne.ThemeVariant) { + s.variant = variant + s.theme = theme + s.apply() +} + +func (s *settings) Scale() float32 { + if s.schema.Scale < 0.0 { + return 1.0 // catching any really old data still using the `-1` value for "auto" scale + } + return s.schema.Scale +} + +func (s *settings) AddChangeListener(listener chan fyne.Settings) { + s.changeListeners.Store(listener, true) // the boolean is just a dummy value here. +} + +func (s *settings) AddListener(listener func(fyne.Settings)) { + s.listeners = append(s.listeners, listener) +} + +func (s *settings) apply() { + s.changeListeners.Range(func(listener chan fyne.Settings, _ bool) bool { + select { + case listener <- s: + default: + l := listener + go func() { l <- s }() + } + return true + }) + + for _, l := range s.listeners { + l(s) + } +} + +func (s *settings) fileChanged() { + s.load() + s.apply() +} + +func (s *settings) setupTheme() { + name := s.schema.ThemeName + if env := os.Getenv("FYNE_THEME"); env != "" { + name = env + } + + variant := app.DefaultVariant() + effectiveTheme := s.theme + if !s.themeSpecified { + effectiveTheme = theme.DefaultTheme() + } + switch name { + case "light": + variant = theme.VariantLight + case "dark": + variant = theme.VariantDark + } + + s.applyTheme(effectiveTheme, variant) +} + +func loadSettings() *settings { + s := &settings{} + s.load() + + return s +} diff --git a/vendor/fyne.io/fyne/v2/app/settings_desktop.go b/vendor/fyne.io/fyne/v2/app/settings_desktop.go new file mode 100644 index 0000000..dc63aaa --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/settings_desktop.go @@ -0,0 +1,80 @@ +//go:build !android && !ios && !mobile && !wasm && !test_web_driver && !tamago && !noos && !tinygo + +package app + +import ( + "os" + "path/filepath" + + "fyne.io/fyne/v2" + "github.com/fsnotify/fsnotify" +) + +func watchFileAddTarget(watcher *fsnotify.Watcher, path string) { + dir := filepath.Dir(path) + ensureDirExists(dir) + + err := watcher.Add(dir) + if err != nil { + fyne.LogError("Settings watch error:", err) + } +} + +func ensureDirExists(dir string) { + if stat, err := os.Stat(dir); err == nil && stat.IsDir() { + return + } + + err := os.MkdirAll(dir, 0o700) + if err != nil { + fyne.LogError("Unable to create settings storage:", err) + } +} + +func watchFile(path string, callback func()) *fsnotify.Watcher { + watcher, err := fsnotify.NewWatcher() + if err != nil { + fyne.LogError("Failed to watch settings file:", err) + return nil + } + + go func() { + for event := range watcher.Events { + if event.Op.Has(fsnotify.Remove) { // if it was deleted then watch again + watcher.Remove(path) // fsnotify returns false positives, see https://github.com/fsnotify/fsnotify/issues/268 + + watchFileAddTarget(watcher, path) + } else { + fyne.Do(callback) + } + } + + err = watcher.Close() + if err != nil { + fyne.LogError("Settings un-watch error:", err) + } + }() + + watchFileAddTarget(watcher, path) + return watcher +} + +func (s *settings) watchSettings() { + if s.themeSpecified { + return // we only watch for theme changes at this time so don't bother + } + s.watcher = watchFile(s.schema.StoragePath(), s.fileChanged) + + a := fyne.CurrentApp() + if a != nil && s != nil && a.Settings() == s { // ignore if testing + watchTheme(s) + } +} + +func (s *settings) stopWatching() { + if s.watcher == nil { + return + } + + s.watcher.(*fsnotify.Watcher).Close() // fsnotify returns false positives, see https://github.com/fsnotify/fsnotify/issues/268 +} diff --git a/vendor/fyne.io/fyne/v2/app/settings_file.go b/vendor/fyne.io/fyne/v2/app/settings_file.go new file mode 100644 index 0000000..7d04bb3 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/settings_file.go @@ -0,0 +1,33 @@ +//go:build !wasm && !test_web_driver && !tamago && !noos && !tinygo + +package app + +import ( + "bufio" + "encoding/json" + "io" + "os" + + "fyne.io/fyne/v2" +) + +func (s *settings) load() { + err := s.loadFromFile(s.schema.StoragePath()) + if err != nil && err != io.EOF { // we can get an EOF in windows settings writes + fyne.LogError("Settings load error:", err) + } + + s.setupTheme() +} + +func (s *settings) loadFromFile(path string) error { + file, err := os.Open(path) // #nosec + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + defer file.Close() + return json.NewDecoder(bufio.NewReader(file)).Decode(&s.schema) +} diff --git a/vendor/fyne.io/fyne/v2/app/settings_mobile.go b/vendor/fyne.io/fyne/v2/app/settings_mobile.go new file mode 100644 index 0000000..642a7e7 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/settings_mobile.go @@ -0,0 +1,11 @@ +//go:build android || ios || mobile + +package app + +func (s *settings) watchSettings() { + // no-op on mobile +} + +func (s *settings) stopWatching() { + // no-op on mobile +} diff --git a/vendor/fyne.io/fyne/v2/app/settings_noos.go b/vendor/fyne.io/fyne/v2/app/settings_noos.go new file mode 100644 index 0000000..119b20f --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/settings_noos.go @@ -0,0 +1,24 @@ +//go:build tamago || noos || tinygo + +package app + +func (s *settings) load() { + s.schema.Scale = 1 +} + +func (s *settings) loadFromFile(_ string) error { + // not supported + return nil +} + +func watchFile(_ string, _ func()) { + // not supported +} + +func (s *settings) watchSettings() { + // not supported +} + +func (s *settings) stopWatching() { + // not supported +} diff --git a/vendor/fyne.io/fyne/v2/app/settings_wasm.go b/vendor/fyne.io/fyne/v2/app/settings_wasm.go new file mode 100644 index 0000000..0bde331 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/settings_wasm.go @@ -0,0 +1,23 @@ +//go:build wasm || test_web_driver + +package app + +func (s *settings) load() { + s.setupTheme() + s.schema.Scale = 1 +} + +func (s *settings) loadFromFile(path string) error { + return nil +} + +func watchFile(path string, callback func()) { +} + +func (s *settings) watchSettings() { + watchTheme(s) +} + +func (s *settings) stopWatching() { + stopWatchingTheme() +} diff --git a/vendor/fyne.io/fyne/v2/app/storage.go b/vendor/fyne.io/fyne/v2/app/storage.go new file mode 100644 index 0000000..52be0e4 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/app/storage.go @@ -0,0 +1,31 @@ +package app + +import ( + "os" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal" + "fyne.io/fyne/v2/storage" +) + +type store struct { + *internal.Docs + a *fyneApp +} + +func (s *store) RootURI() fyne.URI { + if s.a.UniqueID() == "" { + fyne.LogError("Storage API requires a unique ID, use app.NewWithID()", nil) + return storage.NewFileURI(os.TempDir()) + } + + u, err := storage.ParseURI(s.a.storageRoot()) + if err == nil { + return u + } + return storage.NewFileURI(s.a.storageRoot()) +} + +func (s *store) docRootURI() (fyne.URI, error) { + return storage.Child(s.RootURI(), "Documents") +} diff --git a/vendor/fyne.io/fyne/v2/canvas.go b/vendor/fyne.io/fyne/v2/canvas.go new file mode 100644 index 0000000..8fd97d7 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/canvas.go @@ -0,0 +1,58 @@ +package fyne + +import "image" + +// Canvas defines a graphical canvas to which a [CanvasObject] or Container can be added. +// Each canvas has a scale which is automatically applied during the render process. +type Canvas interface { + Content() CanvasObject + SetContent(CanvasObject) + + Refresh(CanvasObject) + + // Focus makes the provided item focused. + // The item has to be added to the contents of the canvas before calling this. + Focus(Focusable) + // FocusNext focuses the next focusable item. + // If no item is currently focused, the first focusable item is focused. + // If the last focusable item is currently focused, the first focusable item is focused. + // + // Since: 2.0 + FocusNext() + // FocusPrevious focuses the previous focusable item. + // If no item is currently focused, the last focusable item is focused. + // If the first focusable item is currently focused, the last focusable item is focused. + // + // Since: 2.0 + FocusPrevious() + Unfocus() + Focused() Focusable + + // Size returns the current size of this canvas + Size() Size + // Scale returns the current scale (multiplication factor) this canvas uses to render + // The pixel size of a [CanvasObject] can be found by multiplying by this value. + Scale() float32 + + // Overlays returns the overlay stack. + Overlays() OverlayStack + + OnTypedRune() func(rune) + SetOnTypedRune(func(rune)) + OnTypedKey() func(*KeyEvent) + SetOnTypedKey(func(*KeyEvent)) + AddShortcut(shortcut Shortcut, handler func(shortcut Shortcut)) + RemoveShortcut(shortcut Shortcut) + + Capture() image.Image + + // PixelCoordinateForPosition returns the x and y pixel coordinate for a given position on this canvas. + // This can be used to find absolute pixel positions or pixel offsets relative to an object top left. + PixelCoordinateForPosition(Position) (int, int) + + // InteractiveArea returns the position and size of the central interactive area. + // Operating system elements may overlap the portions outside this area and widgets should avoid being outside. + // + // Since: 1.4 + InteractiveArea() (Position, Size) +} diff --git a/vendor/fyne.io/fyne/v2/canvas/animation.go b/vendor/fyne.io/fyne/v2/canvas/animation.go new file mode 100644 index 0000000..9b2b2f9 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/canvas/animation.go @@ -0,0 +1,91 @@ +package canvas + +import ( + "image/color" + "time" + + "fyne.io/fyne/v2" +) + +const ( + // DurationStandard is the time a standard interface animation will run. + // + // Since: 2.0 + DurationStandard = time.Millisecond * 300 + // DurationShort is the time a subtle or small transition should use. + // + // Since: 2.0 + DurationShort = time.Millisecond * 150 +) + +// NewColorRGBAAnimation sets up a new animation that will transition from the start to stop Color over +// the specified Duration. The colour transition will move linearly through the RGB colour space. +// The content of fn should apply the color values to an object and refresh it. +// You should call Start() on the returned animation to start it. +// +// Since: 2.0 +func NewColorRGBAAnimation(start, stop color.Color, d time.Duration, fn func(color.Color)) *fyne.Animation { + r1, g1, b1, a1 := start.RGBA() + r2, g2, b2, a2 := stop.RGBA() + + rStart := int(r1 >> 8) + gStart := int(g1 >> 8) + bStart := int(b1 >> 8) + aStart := int(a1 >> 8) + rDelta := float32(int(r2>>8) - rStart) + gDelta := float32(int(g2>>8) - gStart) + bDelta := float32(int(b2>>8) - bStart) + aDelta := float32(int(a2>>8) - aStart) + + return &fyne.Animation{ + Duration: d, + Tick: func(done float32) { + fn(color.RGBA{ + R: scaleChannel(rStart, rDelta, done), G: scaleChannel(gStart, gDelta, done), + B: scaleChannel(bStart, bDelta, done), A: scaleChannel(aStart, aDelta, done), + }) + }, + } +} + +// NewPositionAnimation sets up a new animation that will transition from the start to stop Position over +// the specified Duration. The content of fn should apply the position value to an object for the change +// to be visible. You should call Start() on the returned animation to start it. +// +// Since: 2.0 +func NewPositionAnimation(start, stop fyne.Position, d time.Duration, fn func(fyne.Position)) *fyne.Animation { + xDelta := stop.X - start.X + yDelta := stop.Y - start.Y + + return &fyne.Animation{ + Duration: d, + Tick: func(done float32) { + fn(fyne.NewPos(scaleVal(start.X, xDelta, done), scaleVal(start.Y, yDelta, done))) + }, + } +} + +// NewSizeAnimation sets up a new animation that will transition from the start to stop Size over +// the specified Duration. The content of fn should apply the size value to an object for the change +// to be visible. You should call Start() on the returned animation to start it. +// +// Since: 2.0 +func NewSizeAnimation(start, stop fyne.Size, d time.Duration, fn func(fyne.Size)) *fyne.Animation { + widthDelta := stop.Width - start.Width + heightDelta := stop.Height - start.Height + + return &fyne.Animation{ + Duration: d, + Tick: func(done float32) { + fn(fyne.NewSize(scaleVal(start.Width, widthDelta, done), scaleVal(start.Height, heightDelta, done))) + }, + } +} + +func scaleChannel(start int, diff, done float32) uint8 { + return uint8(start + int(diff*done)) +} + +func scaleVal(start float32, delta, done float32) float32 { + return start + delta*done +} diff --git a/vendor/fyne.io/fyne/v2/canvas/arc.go b/vendor/fyne.io/fyne/v2/canvas/arc.go new file mode 100644 index 0000000..9bd6fa7 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/canvas/arc.go @@ -0,0 +1,95 @@ +package canvas + +import ( + "image/color" + + "fyne.io/fyne/v2" +) + +// Declare conformity with CanvasObject interface +var _ fyne.CanvasObject = (*Arc)(nil) + +// Arc represents a filled arc or annular sector primitive that can be drawn on a Fyne canvas. +// It allows for the creation of circular, ring-shaped or pie-shaped segment, with configurable cutout ratio +// as well as customizable start and end angles to define the arc's length as the absolute difference between the two angles. The arc is always centered on its position. +// The arc is drawn from StartAngle to EndAngle (in degrees, positive is clockwise, negative is counter-clockwise). +// 0°/360 is top, 90° is right, 180° is bottom, 270° is left +// 0°/-360 is top, -90° is left, -180° is bottom, -270° is right +// +// Since: 2.7 +type Arc struct { + baseObject + + FillColor color.Color // The arc fill colour + StartAngle float32 // Start angle in degrees + EndAngle float32 // End angle in degrees + CornerRadius float32 // Radius used to round the corners + StrokeColor color.Color // The arc stroke color + StrokeWidth float32 // The stroke width of the arc + CutoutRatio float32 // Controls what portion of the inner should be cut out. A value of 0.0 results in a pie slice, while 1.0 results in a stroke. +} + +// Hide will set this arc to not be visible. +func (a *Arc) Hide() { + a.baseObject.Hide() + + repaint(a) +} + +// Move the arc to a new position, relative to its parent / canvas. +// The position specifies the **center** of the arc. +func (a *Arc) Move(pos fyne.Position) { + if a.Position() == pos { + return + } + a.baseObject.Move(pos) + + repaint(a) +} + +// Refresh causes this arc to be redrawn with its configured state. +func (a *Arc) Refresh() { + Refresh(a) +} + +// Resize updates the logical size of the arc. +// The arc is always drawn centered on its Position(). +func (a *Arc) Resize(s fyne.Size) { + if s == a.Size() { + return + } + + a.baseObject.Resize(s) + + repaint(a) +} + +// NewArc returns a new Arc instance with the specified start and end angles (in degrees), fill color and cutout ratio. +func NewArc(startAngle, endAngle, cutoutRatio float32, color color.Color) *Arc { + return &Arc{ + StartAngle: startAngle, + EndAngle: endAngle, + FillColor: color, + CutoutRatio: cutoutRatio, + } +} + +// NewPieArc returns a new pie-shaped Arc instance with the specified start and end angles (in degrees), fill color and cutout ratio set to 0. +func NewPieArc(startAngle, endAngle float32, color color.Color) *Arc { + return &Arc{ + StartAngle: startAngle, + EndAngle: endAngle, + FillColor: color, + CutoutRatio: 0.0, + } +} + +// NewDoughnutArc returns a new doughnut-shaped Arc instance with the specified start and end angles (in degrees), fill color and cutout ratio set to 0.5. +func NewDoughnutArc(startAngle, endAngle float32, color color.Color) *Arc { + return &Arc{ + StartAngle: startAngle, + EndAngle: endAngle, + FillColor: color, + CutoutRatio: 0.5, + } +} diff --git a/vendor/fyne.io/fyne/v2/canvas/base.go b/vendor/fyne.io/fyne/v2/canvas/base.go new file mode 100644 index 0000000..5b4a1ee --- /dev/null +++ b/vendor/fyne.io/fyne/v2/canvas/base.go @@ -0,0 +1,69 @@ +// Package canvas contains all of the primitive CanvasObjects that make up a Fyne GUI. +// +// The types implemented in this package are used as building blocks in order +// to build higher order functionality. These types are designed to be +// non-interactive, by design. If additional functionality is required, +// it's usually a sign that this type should be used as part of a custom +// widget. +package canvas // import "fyne.io/fyne/v2/canvas" + +import ( + "fyne.io/fyne/v2" +) + +type baseObject struct { + size fyne.Size // The current size of the canvas object + position fyne.Position // The current position of the object + Hidden bool // Is this object currently hidden + + min fyne.Size // The minimum size this object can be +} + +// Hide will set this object to not be visible. +func (o *baseObject) Hide() { + o.Hidden = true +} + +// MinSize returns the specified minimum size, if set, or {1, 1} otherwise. +func (o *baseObject) MinSize() fyne.Size { + if o.min.IsZero() { + return fyne.Size{Width: 1, Height: 1} + } + + return o.min +} + +// Move the object to a new position, relative to its parent. +func (o *baseObject) Move(pos fyne.Position) { + o.position = pos +} + +// Position gets the current position of this canvas object, relative to its parent. +func (o *baseObject) Position() fyne.Position { + return o.position +} + +// Resize sets a new size for the canvas object. +func (o *baseObject) Resize(size fyne.Size) { + o.size = size +} + +// SetMinSize specifies the smallest size this object should be. +func (o *baseObject) SetMinSize(size fyne.Size) { + o.min = size +} + +// Show will set this object to be visible. +func (o *baseObject) Show() { + o.Hidden = false +} + +// Size returns the current size of this canvas object. +func (o *baseObject) Size() fyne.Size { + return o.size +} + +// Visible returns true if this object is visible, false otherwise. +func (o *baseObject) Visible() bool { + return !o.Hidden +} diff --git a/vendor/fyne.io/fyne/v2/canvas/canvas.go b/vendor/fyne.io/fyne/v2/canvas/canvas.go new file mode 100644 index 0000000..ae679dd --- /dev/null +++ b/vendor/fyne.io/fyne/v2/canvas/canvas.go @@ -0,0 +1,57 @@ +package canvas + +import ( + "image/color" + "math" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/svg" +) + +const ( + // RadiusMaximum can be applied to a canvas corner radius to achieve fully rounded corners. + // This constant represents the maximum possible corner radius, resulting in a circular appearance. + // Since: 2.7 + RadiusMaximum float32 = math.MaxFloat32 +) + +// Refresh instructs the containing canvas to refresh the specified obj. +func Refresh(obj fyne.CanvasObject) { + app := fyne.CurrentApp() + if app == nil || app.Driver() == nil { + return + } + + c := app.Driver().CanvasForObject(obj) + if c != nil { + c.Refresh(obj) + } +} + +// RecolorSVG takes a []byte containing SVG content, and returns +// new SVG content, re-colorized to be monochrome with the given color. +// The content can be assigned to a new fyne.StaticResource with an appropriate name +// to be used in a widget.Button, canvas.Image, etc. +// +// If an error occurs, the returned content will be the original un-modified content, +// and a non-nil error is returned. +// +// Since: 2.6 +func RecolorSVG(svgContent []byte, color color.Color) ([]byte, error) { + return svg.Colorize(svgContent, color) +} + +// repaint instructs the containing canvas to redraw, even if nothing changed. +func repaint(obj fyne.CanvasObject) { + app := fyne.CurrentApp() + if app == nil || app.Driver() == nil { + return + } + + c := app.Driver().CanvasForObject(obj) + if c != nil { + if paint, ok := c.(interface{ SetDirty() }); ok { + paint.SetDirty() + } + } +} diff --git a/vendor/fyne.io/fyne/v2/canvas/circle.go b/vendor/fyne.io/fyne/v2/canvas/circle.go new file mode 100644 index 0000000..415733c --- /dev/null +++ b/vendor/fyne.io/fyne/v2/canvas/circle.go @@ -0,0 +1,95 @@ +package canvas + +import ( + "image/color" + "math" + + "fyne.io/fyne/v2" +) + +// Declare conformity with CanvasObject interface +var _ fyne.CanvasObject = (*Circle)(nil) + +// Circle describes a colored circle primitive in a Fyne canvas +type Circle struct { + Position1 fyne.Position // The current top-left position of the Circle + Position2 fyne.Position // The current bottomright position of the Circle + Hidden bool // Is this circle currently hidden + + FillColor color.Color // The circle fill color + StrokeColor color.Color // The circle stroke color + StrokeWidth float32 // The stroke width of the circle +} + +// NewCircle returns a new Circle instance +func NewCircle(color color.Color) *Circle { + return &Circle{FillColor: color} +} + +// Hide will set this circle to not be visible +func (c *Circle) Hide() { + c.Hidden = true + + repaint(c) +} + +// MinSize for a Circle simply returns Size{1, 1} as there is no +// explicit content +func (c *Circle) MinSize() fyne.Size { + return fyne.NewSize(1, 1) +} + +// Move the circle object to a new position, relative to its parent / canvas +func (c *Circle) Move(pos fyne.Position) { + if c.Position1 == pos { + return + } + + size := c.Size() + c.Position1 = pos + c.Position2 = c.Position1.Add(size) + + repaint(c) +} + +// Position gets the current top-left position of this circle object, relative to its parent / canvas +func (c *Circle) Position() fyne.Position { + return c.Position1 +} + +// Refresh causes this object to be redrawn with its configured state. +func (c *Circle) Refresh() { + Refresh(c) +} + +// Resize sets a new bottom-right position for the circle object +// If it has a stroke width this will cause it to Refresh. +func (c *Circle) Resize(size fyne.Size) { + if size == c.Size() { + return + } + + c.Position2 = c.Position1.Add(size) + + Refresh(c) +} + +// Show will set this circle to be visible +func (c *Circle) Show() { + c.Hidden = false + + c.Refresh() +} + +// Size returns the current size of bounding box for this circle object +func (c *Circle) Size() fyne.Size { + return fyne.NewSize( + float32(math.Abs(float64(c.Position2.X)-float64(c.Position1.X))), + float32(math.Abs(float64(c.Position2.Y)-float64(c.Position1.Y))), + ) +} + +// Visible returns true if this circle is visible, false otherwise +func (c *Circle) Visible() bool { + return !c.Hidden +} diff --git a/vendor/fyne.io/fyne/v2/canvas/gradient.go b/vendor/fyne.io/fyne/v2/canvas/gradient.go new file mode 100644 index 0000000..984600e --- /dev/null +++ b/vendor/fyne.io/fyne/v2/canvas/gradient.go @@ -0,0 +1,238 @@ +package canvas + +import ( + "image" + "image/color" + "math" + + "fyne.io/fyne/v2" +) + +// LinearGradient defines a Gradient travelling straight at a given angle. +// The only supported values for the angle are `0.0` (vertical) and `90.0` (horizontal), currently. +type LinearGradient struct { + baseObject + + StartColor color.Color // The beginning color of the gradient + EndColor color.Color // The end color of the gradient + Angle float64 // The angle of the gradient (0/180 for vertical; 90/270 for horizontal) +} + +// Generate calculates an image of the gradient with the specified width and height. +func (g *LinearGradient) Generate(iw, ih int) image.Image { + w, h := float64(iw), float64(ih) + var generator func(x, y float64) float64 + switch g.Angle { + case 90, -270: // horizontal flipped + generator = func(x, _ float64) float64 { + return (w - x) / w + } + case 270, -90: // horizontal + generator = func(x, _ float64) float64 { + return x / w + } + case 45, -315: // diagonal negative flipped + generator = func(x, y float64) float64 { + return math.Abs((w - x + y) / (w + h)) // ((w+h)-(x+h-y)) / (w+h) + } + case 225, -135: // diagonal negative + generator = func(x, y float64) float64 { + return math.Abs((x + h - y) / (w + h)) + } + case 135, -225: // diagonal positive flipped + generator = func(x, y float64) float64 { + return math.Abs((w + h - (x + y)) / (w + h)) + } + case 315, -45: // diagonal positive + generator = func(x, y float64) float64 { + return math.Abs((x + y) / (w + h)) + } + case 180, -180: // vertical flipped + generator = func(_, y float64) float64 { + return (h - y) / h + } + default: // vertical + generator = func(_, y float64) float64 { + return y / h + } + } + return computeGradient(generator, iw, ih, g.StartColor, g.EndColor) +} + +// Hide will set this gradient to not be visible +func (g *LinearGradient) Hide() { + g.baseObject.Hide() + + repaint(g) +} + +// Move the gradient to a new position, relative to its parent / canvas +func (g *LinearGradient) Move(pos fyne.Position) { + if g.Position() == pos { + return + } + + g.baseObject.Move(pos) + + repaint(g) +} + +// Resize resizes the gradient to a new size. +func (g *LinearGradient) Resize(size fyne.Size) { + if size == g.Size() { + return + } + g.baseObject.Resize(size) + + // refresh needed to invalidate cached textures + g.Refresh() +} + +// Refresh causes this gradient to be redrawn with its configured state. +func (g *LinearGradient) Refresh() { + Refresh(g) +} + +// RadialGradient defines a Gradient travelling radially from a center point outward. +type RadialGradient struct { + baseObject + + StartColor color.Color // The beginning color of the gradient + EndColor color.Color // The end color of the gradient + // The offset of the center for generation of the gradient. + // This is not a DP measure but relates to the width/height. + // A value of 0.5 would move the center by the half width/height. + CenterOffsetX, CenterOffsetY float64 +} + +// Generate calculates an image of the gradient with the specified width and height. +func (g *RadialGradient) Generate(iw, ih int) image.Image { + w, h := float64(iw), float64(ih) + // define center plus offset + centerX := w/2 + w*g.CenterOffsetX + centerY := h/2 + h*g.CenterOffsetY + + // handle negative offsets + var a, b float64 + if g.CenterOffsetX < 0 { + a = w - centerX + } else { + a = centerX + } + if g.CenterOffsetY < 0 { + b = h - centerY + } else { + b = centerY + } + + generator := func(x, y float64) float64 { + // calculate distance from center for gradient multiplier + dx, dy := centerX-x, centerY-y + da := math.Sqrt(dx*dx + dy*dy*a*a/b/b) + if da > a { + return 1 + } + return da / a + } + return computeGradient(generator, iw, ih, g.StartColor, g.EndColor) +} + +// Hide will set this gradient to not be visible +func (g *RadialGradient) Hide() { + g.baseObject.Hide() + + repaint(g) +} + +// Move the gradient to a new position, relative to its parent / canvas +func (g *RadialGradient) Move(pos fyne.Position) { + g.baseObject.Move(pos) + + repaint(g) +} + +// Resize resizes the gradient to a new size. +func (g *RadialGradient) Resize(size fyne.Size) { + if size == g.Size() { + return + } + g.baseObject.Resize(size) + + // refresh needed to invalidate cached textures + g.Refresh() +} + +// Refresh causes this gradient to be redrawn with its configured state. +func (g *RadialGradient) Refresh() { + Refresh(g) +} + +func calculatePixel(d float64, startColor, endColor color.Color) color.Color { + // fetch RGBA values + aR, aG, aB, aA := startColor.RGBA() + bR, bG, bB, bA := endColor.RGBA() + + // Get difference + dR := float64(bR) - float64(aR) + dG := float64(bG) - float64(aG) + dB := float64(bB) - float64(aB) + dA := float64(bA) - float64(aA) + + // Apply gradations + pixel := &color.RGBA64{ + R: uint16(float64(aR) + d*dR), + B: uint16(float64(aB) + d*dB), + G: uint16(float64(aG) + d*dG), + A: uint16(float64(aA) + d*dA), + } + + return pixel +} + +func computeGradient(generator func(x, y float64) float64, w, h int, startColor, endColor color.Color) image.Image { + img := image.NewNRGBA(image.Rect(0, 0, w, h)) + + if startColor == nil && endColor == nil { + return img + } else if startColor == nil { + startColor = color.Transparent + } else if endColor == nil { + endColor = color.Transparent + } + + for x := 0; x < w; x++ { + for y := 0; y < h; y++ { + distance := generator(float64(x)+0.5, float64(y)+0.5) + img.Set(x, y, calculatePixel(distance, startColor, endColor)) + } + } + return img +} + +// NewHorizontalGradient creates a new horizontally travelling linear gradient. +// The start color will be at the left of the gradient and the end color will be at the right. +func NewHorizontalGradient(start, end color.Color) *LinearGradient { + g := &LinearGradient{StartColor: start, EndColor: end} + g.Angle = 270 + return g +} + +// NewLinearGradient creates a linear gradient at the specified angle. +// The angle parameter is the degree angle along which the gradient is calculated. +// A NewHorizontalGradient uses 270 degrees and NewVerticalGradient is 0 degrees. +func NewLinearGradient(start, end color.Color, angle float64) *LinearGradient { + g := &LinearGradient{StartColor: start, EndColor: end} + g.Angle = angle + return g +} + +// NewRadialGradient creates a new radial gradient. +func NewRadialGradient(start, end color.Color) *RadialGradient { + return &RadialGradient{StartColor: start, EndColor: end} +} + +// NewVerticalGradient creates a new vertically travelling linear gradient. +// The start color will be at the top of the gradient and the end color will be at the bottom. +func NewVerticalGradient(start color.Color, end color.Color) *LinearGradient { + return &LinearGradient{StartColor: start, EndColor: end} +} diff --git a/vendor/fyne.io/fyne/v2/canvas/image.go b/vendor/fyne.io/fyne/v2/canvas/image.go new file mode 100644 index 0000000..ea0bdb2 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/canvas/image.go @@ -0,0 +1,399 @@ +package canvas + +import ( + "bytes" + "errors" + "image" + _ "image/jpeg" // avoid users having to import when using image widget + _ "image/png" // avoid the same for PNG images + "io" + "os" + "path/filepath" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/cache" + "fyne.io/fyne/v2/internal/scale" + "fyne.io/fyne/v2/internal/svg" + "fyne.io/fyne/v2/storage" +) + +// ImageFill defines the different type of ways an image can stretch to fill its space. +type ImageFill int + +const ( + // ImageFillStretch will scale the image to match the Size() values. + // This is the default and does not maintain aspect ratio. + ImageFillStretch ImageFill = iota + // ImageFillContain makes the image fit within the object Size(), + // centrally and maintaining aspect ratio. + // There may be transparent sections top and bottom or left and right. + ImageFillContain // (Fit) + // ImageFillOriginal ensures that the container grows to the pixel dimensions + // required to fit the original image. The aspect of the image will be maintained so, + // as with ImageFillContain there may be transparent areas around the image. + // Note that the minSize may be smaller than the image dimensions if scale > 1. + ImageFillOriginal + + // ImageFillCover maintains the image aspect ratio whilst filling the space. + // The image content will be centered on the available space meaning that an equal amount of top and bottom + // or left and right will be clipped if the output aspect ratio does not match the source image. + // Since: 2.7 + ImageFillCover +) + +// ImageScale defines the different scaling filters used to scaling images +type ImageScale int32 + +const ( + // ImageScaleSmooth will scale the image using ApproxBiLinear filter (or GL equivalent) + ImageScaleSmooth ImageScale = iota + // ImageScalePixels will scale the image using NearestNeighbor filter (or GL equivalent) + ImageScalePixels + // ImageScaleFastest will scale the image using hardware GPU if available + // + // Since: 2.0 + ImageScaleFastest +) + +// Declare conformity with CanvasObject interface +var _ fyne.CanvasObject = (*Image)(nil) + +// Image describes a drawable image area that can render in a Fyne canvas +// The image may be a vector or a bitmap representation, it will fill the area. +// The fill mode can be changed by setting FillMode to a different ImageFill. +type Image struct { + baseObject + + aspect float32 + icon *svg.Decoder + isSVG bool + + // one of the following sources will provide our image data + File string // Load the image from a file + Resource fyne.Resource // Load the image from an in-memory resource + Image image.Image // Specify a loaded image to use in this canvas object + + Translucency float64 // Set a translucency value > 0.0 to fade the image + FillMode ImageFill // Specify how the image should expand to fill or fit the available space + ScaleMode ImageScale // Specify the type of scaling interpolation applied to the image + + // CornerRadius specifies a radius to apply to round corners of the image. + // + // Since: 2.7 + CornerRadius float32 + + previousRender bool // did we successfully draw before? if so a nil content will need a reset +} + +// Alpha is a convenience function that returns the alpha value for an image +// based on its Translucency value. The result is 1.0 - Translucency. +func (i *Image) Alpha() float64 { + return 1.0 - i.Translucency +} + +// Aspect will return the original content aspect after it was last refreshed. +// +// Since: 2.4 +func (i *Image) Aspect() float32 { + if i.aspect == 0 { + i.Refresh() + } + return i.aspect +} + +// Hide will set this image to not be visible +func (i *Image) Hide() { + i.baseObject.Hide() + + repaint(i) +} + +// MinSize returns the specified minimum size, if set, or {1, 1} otherwise. +func (i *Image) MinSize() fyne.Size { + if i.Image == nil || i.aspect == 0 { + if i.File != "" || i.Resource != nil { + i.Refresh() + } + } + return i.baseObject.MinSize() +} + +// Move the image object to a new position, relative to its parent top, left corner. +func (i *Image) Move(pos fyne.Position) { + if i.Position() == pos { + return + } + + i.baseObject.Move(pos) + + repaint(i) +} + +// Refresh causes this image to be redrawn with its configured state. +func (i *Image) Refresh() { + rc, err := i.updateReader() + if err != nil { + fyne.LogError("Failed to load image", err) + return + } + if rc != nil { + rcMem := rc + defer rcMem.Close() + } + + if i.File != "" || i.Resource != nil || i.Image != nil { + r, err := i.updateAspectAndMinSize(rc) + if err != nil { + fyne.LogError("Failed to load image", err) + return + } + rc = io.NopCloser(r) + } else if i.previousRender { + i.previousRender = false + + Refresh(i) + return + } else { + return + } + + if i.File != "" || i.Resource != nil { + size := i.Size() + width := size.Width + height := size.Height + + if width == 0 || height == 0 { + return + } + + if i.isSVG { + tex, err := i.renderSVG(width, height) + if err != nil { + fyne.LogError("Failed to render SVG", err) + return + } + i.Image = tex + } else { + if rc == nil { + return + } + + img, _, err := image.Decode(rc) + if err != nil { + fyne.LogError("Failed to render image", err) + return + } + i.Image = img + } + } + + i.previousRender = true + Refresh(i) +} + +// Resize on an image will scale the content or reposition it according to FillMode. +// It will normally cause a Refresh to ensure the pixels are recalculated. +func (i *Image) Resize(s fyne.Size) { + if s == i.Size() { + return + } + i.baseObject.Resize(s) + if i.FillMode == ImageFillOriginal && i.Size().Height > 2 { // we can just ask for a GPU redraw to align + Refresh(i) + return + } + + i.baseObject.Resize(s) + if i.isSVG || i.Image == nil { + i.Refresh() // we need to rasterise at the new size + } else { + Refresh(i) // just re-size using GPU scaling + } +} + +// NewImageFromFile creates a new image from a local file. +// Images returned from this method will scale to fit the canvas object. +// The method for scaling can be set using the Fill field. +func NewImageFromFile(file string) *Image { + return &Image{File: file} +} + +// NewImageFromURI creates a new image from named resource. +// File URIs will read the file path and other schemes will download the data into a resource. +// HTTP and HTTPs URIs will use the GET method by default to request the resource. +// Images returned from this method will scale to fit the canvas object. +// The method for scaling can be set using the Fill field. +// +// Since: 2.0 +func NewImageFromURI(uri fyne.URI) *Image { + if uri.Scheme() == "file" && len(uri.String()) > 7 { + return NewImageFromFile(uri.Path()) + } + + var read io.ReadCloser + + read, err := storage.Reader(uri) // attempt unknown / http file type + if err != nil { + fyne.LogError("Failed to open image URI", err) + return &Image{} + } + + defer read.Close() + return NewImageFromReader(read, filepath.Base(uri.String())) +} + +// NewImageFromReader creates a new image from a data stream. +// The name parameter is required to uniquely identify this image (for caching etc.). +// If the image in this io.Reader is an SVG, the name should end ".svg". +// Images returned from this method will scale to fit the canvas object. +// The method for scaling can be set using the Fill field. +// +// Since: 2.0 +func NewImageFromReader(read io.Reader, name string) *Image { + data, err := io.ReadAll(read) + if err != nil { + fyne.LogError("Unable to read image data", err) + return nil + } + + res := &fyne.StaticResource{ + StaticName: name, + StaticContent: data, + } + + return NewImageFromResource(res) +} + +// NewImageFromResource creates a new image by loading the specified resource. +// Images returned from this method will scale to fit the canvas object. +// The method for scaling can be set using the Fill field. +func NewImageFromResource(res fyne.Resource) *Image { + return &Image{Resource: res} +} + +// NewImageFromImage returns a new Image instance that is rendered from the Go +// image.Image passed in. +// Images returned from this method will scale to fit the canvas object. +// The method for scaling can be set using the Fill field. +func NewImageFromImage(img image.Image) *Image { + return &Image{Image: img} +} + +func (i *Image) name() string { + if i.Resource != nil { + return i.Resource.Name() + } else if i.File != "" { + return i.File + } + return "" +} + +func (i *Image) updateReader() (io.ReadCloser, error) { + i.isSVG = false + if i.Resource != nil { + i.isSVG = svg.IsResourceSVG(i.Resource) + content := i.Resource.Content() + if res, ok := i.Resource.(fyne.ThemedResource); i.isSVG && ok { + th := cache.WidgetTheme(i) + if th != nil { + col := th.Color(res.ThemeColorName(), fyne.CurrentApp().Settings().ThemeVariant()) + var err error + content, err = svg.Colorize(content, col) + if err != nil { + fyne.LogError("", err) + } + } + } + return io.NopCloser(bytes.NewReader(content)), nil + } else if i.File != "" { + var err error + + fd, err := os.Open(i.File) + if err != nil { + return nil, err + } + i.isSVG = svg.IsFileSVG(i.File) + return fd, nil + } + return nil, nil +} + +func (i *Image) updateAspectAndMinSize(reader io.Reader) (io.Reader, error) { + var pixWidth, pixHeight int + + if reader != nil { + r, width, height, aspect, err := i.imageDetailsFromReader(reader) + if err != nil { + return nil, err + } + reader = r + i.aspect = aspect + pixWidth, pixHeight = width, height + } else if i.Image != nil { + original := i.Image.Bounds().Size() + i.aspect = float32(original.X) / float32(original.Y) + pixWidth, pixHeight = original.X, original.Y + } else { + return nil, errors.New("no matching image source") + } + + if i.FillMode == ImageFillOriginal { + i.SetMinSize(scale.ToFyneSize(i, pixWidth, pixHeight)) + } + return reader, nil +} + +func (i *Image) imageDetailsFromReader(source io.Reader) (reader io.Reader, width, height int, aspect float32, err error) { + if source == nil { + return nil, 0, 0, 0, errors.New("no matching reading reader") + } + + if i.isSVG { + var err error + + i.icon, err = svg.NewDecoder(source) + if err != nil { + return nil, 0, 0, 0, err + } + config := i.icon.Config() + width, height = config.Width, config.Height + aspect = config.Aspect + } else { + var buf bytes.Buffer + tee := io.TeeReader(source, &buf) + reader = io.MultiReader(&buf, source) + + config, _, err := image.DecodeConfig(tee) + if err != nil { + return nil, 0, 0, 0, err + } + width, height = config.Width, config.Height + aspect = float32(width) / float32(height) + } + return reader, width, height, aspect, err +} + +func (i *Image) renderSVG(width, height float32) (image.Image, error) { + c := fyne.CurrentApp().Driver().CanvasForObject(i) + screenWidth, screenHeight := int(width), int(height) + if c != nil { + // We want real output pixel count not just the screen coordinate space (i.e. macOS Retina) + screenWidth, screenHeight = c.PixelCoordinateForPosition(fyne.Position{X: width, Y: height}) + } else { // no canvas info, assume HiDPI + screenWidth *= 2 + screenHeight *= 2 + } + + tex := cache.GetSvg(i.name(), i, screenWidth, screenHeight) + if tex != nil { + return tex, nil + } + + var err error + tex, err = i.icon.Draw(screenWidth, screenHeight) + if err != nil { + return nil, err + } + cache.SetSvg(i.name(), i, tex, screenWidth, screenHeight) + return tex, nil +} diff --git a/vendor/fyne.io/fyne/v2/canvas/line.go b/vendor/fyne.io/fyne/v2/canvas/line.go new file mode 100644 index 0000000..3295865 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/canvas/line.go @@ -0,0 +1,108 @@ +package canvas + +import ( + "image/color" + "math" + + "fyne.io/fyne/v2" +) + +// Declare conformity with CanvasObject interface +var _ fyne.CanvasObject = (*Line)(nil) + +// Line describes a colored line primitive in a Fyne canvas. +// Lines are special as they can have a negative width or height to indicate +// an inverse slope (i.e. slope up vs down). +type Line struct { + Position1 fyne.Position // The current top-left position of the Line + Position2 fyne.Position // The current bottom-right position of the Line + Hidden bool // Is this Line currently hidden + + StrokeColor color.Color // The line stroke color + StrokeWidth float32 // The stroke width of the line +} + +// Size returns the current size of bounding box for this line object +func (l *Line) Size() fyne.Size { + return fyne.NewSize( + float32(math.Abs(float64(l.Position2.X)-float64(l.Position1.X))), + float32(math.Abs(float64(l.Position2.Y)-float64(l.Position1.Y))), + ) +} + +// Resize sets a new bottom-right position for the line object, then it will then be refreshed. +func (l *Line) Resize(size fyne.Size) { + if size == l.Size() { + return + } + + if l.Position1.X <= l.Position2.X { + l.Position2.X = l.Position1.X + size.Width + } else { + l.Position1.X = l.Position2.X + size.Width + } + if l.Position1.Y <= l.Position2.Y { + l.Position2.Y = l.Position1.Y + size.Height + } else { + l.Position1.Y = l.Position2.Y + size.Height + } + Refresh(l) +} + +// Position gets the current top-left position of this line object, relative to its parent / canvas +func (l *Line) Position() fyne.Position { + return fyne.NewPos(fyne.Min(l.Position1.X, l.Position2.X), fyne.Min(l.Position1.Y, l.Position2.Y)) +} + +// Move the line object to a new position, relative to its parent / canvas +func (l *Line) Move(pos fyne.Position) { + oldPos := l.Position() + if oldPos == pos { + return + } + + deltaX := pos.X - oldPos.X + deltaY := pos.Y - oldPos.Y + + l.Position1 = l.Position1.AddXY(deltaX, deltaY) + l.Position2 = l.Position2.AddXY(deltaX, deltaY) + repaint(l) +} + +// MinSize for a Line simply returns Size{1, 1} as there is no +// explicit content +func (l *Line) MinSize() fyne.Size { + return fyne.NewSize(1, 1) +} + +// Visible returns true if this line// Show will set this circle to be visible is visible, false otherwise +func (l *Line) Visible() bool { + return !l.Hidden +} + +// Show will set this line to be visible +func (l *Line) Show() { + l.Hidden = false + + l.Refresh() +} + +// Hide will set this line to not be visible +func (l *Line) Hide() { + l.Hidden = true + + repaint(l) +} + +// Refresh causes this line to be redrawn with its configured state. +func (l *Line) Refresh() { + Refresh(l) +} + +// NewLine returns a new Line instance +func NewLine(color color.Color) *Line { + return &Line{ + StrokeColor: color, + StrokeWidth: 1, + } +} diff --git a/vendor/fyne.io/fyne/v2/canvas/polygon.go b/vendor/fyne.io/fyne/v2/canvas/polygon.go new file mode 100644 index 0000000..37b9cb2 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/canvas/polygon.go @@ -0,0 +1,71 @@ +package canvas + +import ( + "image/color" + + "fyne.io/fyne/v2" +) + +// Declare conformity with CanvasObject interface +var _ fyne.CanvasObject = (*Polygon)(nil) + +// Polygon describes a colored regular polygon primitive in a Fyne canvas. +// The rendered portion will be in the center of the available space. +// +// Since: 2.7 +type Polygon struct { + baseObject + + FillColor color.Color // The polygon fill color + StrokeColor color.Color // The polygon stroke color + StrokeWidth float32 // The stroke width of the polygon + CornerRadius float32 // The radius of the polygon corners + Angle float32 // Angle of polygon, in degrees (positive means clockwise, negative means counter-clockwise direction). + Sides uint // Amount of sides of polygon. +} + +// Hide will set this polygon to not be visible +func (r *Polygon) Hide() { + r.baseObject.Hide() + + repaint(r) +} + +// Move the polygon to a new position, relative to its parent / canvas +func (r *Polygon) Move(pos fyne.Position) { + if r.Position() == pos { + return + } + + r.baseObject.Move(pos) + + repaint(r) +} + +// Refresh causes this polygon to be redrawn with its configured state. +func (r *Polygon) Refresh() { + Refresh(r) +} + +// Resize on a polygon updates the new size of this object. +// If it has a stroke width this will cause it to Refresh. +func (r *Polygon) Resize(s fyne.Size) { + if s == r.Size() { + return + } + + r.baseObject.Resize(s) + if r.StrokeWidth == 0 { + return + } + + Refresh(r) +} + +// NewPolygon returns a new Polygon instance +func NewPolygon(sides uint, color color.Color) *Polygon { + return &Polygon{ + Sides: sides, + FillColor: color, + } +} diff --git a/vendor/fyne.io/fyne/v2/canvas/raster.go b/vendor/fyne.io/fyne/v2/canvas/raster.go new file mode 100644 index 0000000..8d3dfc9 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/canvas/raster.go @@ -0,0 +1,200 @@ +package canvas + +import ( + "image" + "image/color" + "image/draw" + + "fyne.io/fyne/v2" +) + +// Declare conformity with CanvasObject interface +var _ fyne.CanvasObject = (*Raster)(nil) + +// Raster describes a raster image area that can render in a Fyne canvas +type Raster struct { + baseObject + + // Render the raster image from code + Generator func(w, h int) image.Image + + // Set a translucency value > 0.0 to fade the raster + Translucency float64 + // Specify the type of scaling interpolation applied to the raster if it is not full-size + // Since: 1.4.1 + ScaleMode ImageScale +} + +// Alpha is a convenience function that returns the alpha value for a raster +// based on its Translucency value. The result is 1.0 - Translucency. +func (r *Raster) Alpha() float64 { + return 1.0 - r.Translucency +} + +// Hide will set this raster to not be visible +func (r *Raster) Hide() { + r.baseObject.Hide() + + repaint(r) +} + +// Move the raster to a new position, relative to its parent / canvas +func (r *Raster) Move(pos fyne.Position) { + if r.Position() == pos { + return + } + + r.baseObject.Move(pos) + + repaint(r) +} + +// Resize on a raster image causes the new size to be set and then calls Refresh. +// This causes the underlying data to be recalculated and a new output to be drawn. +func (r *Raster) Resize(s fyne.Size) { + if s == r.Size() { + return + } + + r.baseObject.Resize(s) + Refresh(r) +} + +// Refresh causes this raster to be redrawn with its configured state. +func (r *Raster) Refresh() { + Refresh(r) +} + +// NewRaster returns a new Image instance that is rendered dynamically using +// the specified generate function. +// Images returned from this method should draw dynamically to fill the width +// and height parameters passed to pixelColor. +func NewRaster(generate func(w, h int) image.Image) *Raster { + return &Raster{Generator: generate} +} + +type pixelRaster struct { + r *Raster + + img draw.Image +} + +// NewRasterWithPixels returns a new Image instance that is rendered dynamically +// by iterating over the specified pixelColor function for each x, y pixel. +// Images returned from this method should draw dynamically to fill the width +// and height parameters passed to pixelColor. +func NewRasterWithPixels(pixelColor func(x, y, w, h int) color.Color) *Raster { + pix := &pixelRaster{} + pix.r = &Raster{ + Generator: func(w, h int) image.Image { + if pix.img == nil || pix.img.Bounds().Size().X != w || pix.img.Bounds().Size().Y != h { + // raster first pixel, figure out color type + var dst draw.Image + rect := image.Rect(0, 0, w, h) + switch pixelColor(0, 0, w, h).(type) { + case color.Alpha: + dst = image.NewAlpha(rect) + case color.Alpha16: + dst = image.NewAlpha16(rect) + case color.CMYK: + dst = image.NewCMYK(rect) + case color.Gray: + dst = image.NewGray(rect) + case color.Gray16: + dst = image.NewGray16(rect) + case color.NRGBA: + dst = image.NewNRGBA(rect) + case color.NRGBA64: + dst = image.NewNRGBA64(rect) + case color.RGBA: + dst = image.NewRGBA(rect) + case color.RGBA64: + dst = image.NewRGBA64(rect) + default: + dst = image.NewRGBA(rect) + } + pix.img = dst + } + + for y := 0; y < h; y++ { + for x := 0; x < w; x++ { + pix.img.Set(x, y, pixelColor(x, y, w, h)) + } + } + + return pix.img + }, + } + return pix.r +} + +type subImg interface { + SubImage(r image.Rectangle) image.Image +} + +// NewRasterFromImage returns a new Raster instance that is rendered from the Go +// image.Image passed in. +// Rasters returned from this method will map pixel for pixel to the screen +// starting img.Bounds().Min pixels from the top left of the canvas object. +// Truncates rather than scales the image. +// If smaller than the target space, the image will be padded with zero-pixels to the target size. +func NewRasterFromImage(img image.Image) *Raster { + return &Raster{ + Generator: func(w int, h int) image.Image { + bounds := img.Bounds() + + rect := image.Rect(0, 0, w, h) + + switch { + case w == bounds.Max.X && h == bounds.Max.Y: + return img + case w >= bounds.Max.X && h >= bounds.Max.Y: + // try quickly truncating + if sub, ok := img.(subImg); ok { + return sub.SubImage(image.Rectangle{ + Min: bounds.Min, + Max: image.Point{ + X: bounds.Min.X + w, + Y: bounds.Min.Y + h, + }, + }) + } + default: + if !rect.Overlaps(bounds) { + return image.NewUniform(color.RGBA{}) + } + bounds = bounds.Intersect(rect) + } + + // respect the user's pixel format (if possible) + var dst draw.Image + switch i := img.(type) { + case *image.Alpha: + dst = image.NewAlpha(rect) + case *image.Alpha16: + dst = image.NewAlpha16(rect) + case *image.CMYK: + dst = image.NewCMYK(rect) + case *image.Gray: + dst = image.NewGray(rect) + case *image.Gray16: + dst = image.NewGray16(rect) + case *image.NRGBA: + dst = image.NewNRGBA(rect) + case *image.NRGBA64: + dst = image.NewNRGBA64(rect) + case *image.Paletted: + dst = image.NewPaletted(rect, i.Palette) + case *image.RGBA: + dst = image.NewRGBA(rect) + case *image.RGBA64: + dst = image.NewRGBA64(rect) + default: + dst = image.NewRGBA(rect) + } + + draw.Draw(dst, bounds, img, bounds.Min, draw.Over) + return dst + }, + } +} diff --git a/vendor/fyne.io/fyne/v2/canvas/rectangle.go b/vendor/fyne.io/fyne/v2/canvas/rectangle.go new file mode 100644 index 0000000..59ce921 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/canvas/rectangle.go @@ -0,0 +1,95 @@ +package canvas + +import ( + "image/color" + + "fyne.io/fyne/v2" +) + +// Declare conformity with CanvasObject interface +var _ fyne.CanvasObject = (*Rectangle)(nil) + +// Rectangle describes a colored rectangle primitive in a Fyne canvas +type Rectangle struct { + baseObject + + FillColor color.Color // The rectangle fill color + StrokeColor color.Color // The rectangle stroke color + StrokeWidth float32 // The stroke width of the rectangle + // The radius of the rectangle corners + // + // Since: 2.4 + CornerRadius float32 + + // Enforce an aspect ratio for the rectangle, the content will be made shorter or narrower + // to meet the requested aspect, if set. + // + // Since: 2.7 + Aspect float32 + + // The radius of the rectangle top-right corner only. + // + // Since: 2.7 + TopRightCornerRadius float32 + + // The radius of the rectangle top-left corner only. + // + // Since: 2.7 + TopLeftCornerRadius float32 + + // The radius of the rectangle bottom-right corner only. + // + // Since: 2.7 + BottomRightCornerRadius float32 + + // The radius of the rectangle bottom-left corner only. + // + // Since: 2.7 + BottomLeftCornerRadius float32 +} + +// Hide will set this rectangle to not be visible +func (r *Rectangle) Hide() { + r.baseObject.Hide() + + repaint(r) +} + +// Move the rectangle to a new position, relative to its parent / canvas +func (r *Rectangle) Move(pos fyne.Position) { + if r.Position() == pos { + return + } + + r.baseObject.Move(pos) + + repaint(r) +} + +// Refresh causes this rectangle to be redrawn with its configured state. +func (r *Rectangle) Refresh() { + Refresh(r) +} + +// Resize on a rectangle updates the new size of this object. +// If it has a stroke width this will cause it to Refresh. +// If Aspect is non-zero it may cause the rectangle to be smaller than the requested size. +func (r *Rectangle) Resize(s fyne.Size) { + if s == r.Size() { + return + } + + r.baseObject.Resize(s) + if r.StrokeWidth == 0 { + return + } + + Refresh(r) +} + +// NewRectangle returns a new Rectangle instance +func NewRectangle(color color.Color) *Rectangle { + return &Rectangle{ + FillColor: color, + } +} diff --git a/vendor/fyne.io/fyne/v2/canvas/square.go b/vendor/fyne.io/fyne/v2/canvas/square.go new file mode 100644 index 0000000..4931392 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/canvas/square.go @@ -0,0 +1,13 @@ +package canvas + +import "image/color" + +// NewSquare returns a new Rectangle instance that has a square aspect ratio. +// +// Since: 2.7 +func NewSquare(color color.Color) *Rectangle { + return &Rectangle{ + Aspect: 1, + FillColor: color, + } +} diff --git a/vendor/fyne.io/fyne/v2/canvas/text.go b/vendor/fyne.io/fyne/v2/canvas/text.go new file mode 100644 index 0000000..05c8bfa --- /dev/null +++ b/vendor/fyne.io/fyne/v2/canvas/text.go @@ -0,0 +1,85 @@ +package canvas + +import ( + "image/color" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/theme" +) + +// Declare conformity with CanvasObject interface +var _ fyne.CanvasObject = (*Text)(nil) + +// Text describes a text primitive in a Fyne canvas. +// A text object can have a style set which will apply to the whole string. +// No formatting or text parsing will be performed +type Text struct { + baseObject + Alignment fyne.TextAlign // The alignment of the text content + + Color color.Color // The main text draw color + Text string // The string content of this Text + TextSize float32 // Size of the text - if the Canvas scale is 1.0 this will be equivalent to point size + TextStyle fyne.TextStyle // The style of the text content + + // FontSource defines a resource that can be used instead of the theme for looking up the font. + // When a font source is set the `TextStyle` may not be effective, as it will be limited to the styles + // present in the data provided. + // + // Since: 2.5 + FontSource fyne.Resource +} + +// Hide will set this text to not be visible +func (t *Text) Hide() { + t.baseObject.Hide() + + repaint(t) +} + +// MinSize returns the minimum size of this text object based on its font size and content. +// This is normally determined by the render implementation. +func (t *Text) MinSize() fyne.Size { + s, _ := fyne.CurrentApp().Driver().RenderedTextSize(t.Text, t.TextSize, t.TextStyle, t.FontSource) + return s +} + +// Move the text to a new position, relative to its parent / canvas +func (t *Text) Move(pos fyne.Position) { + if t.Position() == pos { + return + } + + t.baseObject.Move(pos) + + repaint(t) +} + +// Resize on a text updates the new size of this object, which may not result in a visual change, depending on alignment. +func (t *Text) Resize(s fyne.Size) { + if s == t.Size() { + return + } + + t.baseObject.Resize(s) + Refresh(t) +} + +// SetMinSize has no effect as the smallest size this canvas object can be is based on its font size and content. +func (t *Text) SetMinSize(fyne.Size) { + // no-op +} + +// Refresh causes this text to be redrawn with its configured state. +func (t *Text) Refresh() { + Refresh(t) +} + +// NewText returns a new Text implementation +func NewText(text string, color color.Color) *Text { + return &Text{ + Color: color, + Text: text, + TextSize: theme.TextSize(), + } +} diff --git a/vendor/fyne.io/fyne/v2/canvasobject.go b/vendor/fyne.io/fyne/v2/canvasobject.go new file mode 100644 index 0000000..0566627 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/canvasobject.go @@ -0,0 +1,107 @@ +package fyne + +// CanvasObject describes any graphical object that can be added to a canvas. +// Objects have a size and position that can be controlled through this API. +// MinSize is used to determine the minimum size which this object should be displayed. +// An object will be visible by default but can be hidden with Hide() and re-shown with Show(). +// +// Note: If this object is controlled as part of a Layout you should not call +// Resize(Size) or Move(Position). +type CanvasObject interface { + // geometry + + // MinSize returns the minimum size this object needs to be drawn. + MinSize() Size + // Move moves this object to the given position relative to its parent. + // This should only be called if your object is not in a container with a layout manager. + Move(Position) + // Position returns the current position of the object relative to its parent. + Position() Position + // Resize resizes this object to the given size. + // This should only be called if your object is not in a container with a layout manager. + Resize(Size) + // Size returns the current size of this object. + Size() Size + + // visibility + + // Hide hides this object. + Hide() + // Visible returns whether this object is visible or not. + Visible() bool + // Show shows this object. + Show() + + // Refresh must be called if this object should be redrawn because its inner state changed. + Refresh() +} + +// Disableable describes any [CanvasObject] that can be disabled. +// This is primarily used with objects that also implement the Tappable interface. +type Disableable interface { + Enable() + Disable() + Disabled() bool +} + +// DoubleTappable describes any [CanvasObject] that can also be double tapped. +type DoubleTappable interface { + DoubleTapped(*PointEvent) +} + +// Draggable indicates that a [CanvasObject] can be dragged. +// This is used for any item that the user has indicated should be moved across the screen. +type Draggable interface { + Dragged(*DragEvent) + DragEnd() +} + +// Focusable describes any [CanvasObject] that can respond to being focused. +// It will receive the FocusGained and FocusLost events appropriately. +// When focused it will also have TypedRune called as text is input and +// TypedKey called when other keys are pressed. +// +// Note: You must not change canvas state (including overlays or focus) in FocusGained or FocusLost +// or you would end up with a dead-lock. +type Focusable interface { + // FocusGained is a hook called by the focus handling logic after this object gained the focus. + FocusGained() + // FocusLost is a hook called by the focus handling logic after this object lost the focus. + FocusLost() + + // TypedRune is a hook called by the input handling logic on text input events if this object is focused. + TypedRune(rune) + // TypedKey is a hook called by the input handling logic on key events if this object is focused. + TypedKey(*KeyEvent) +} + +// Scrollable describes any [CanvasObject] that can also be scrolled. +// This is mostly used to implement the widget.ScrollContainer. +type Scrollable interface { + Scrolled(*ScrollEvent) +} + +// SecondaryTappable describes a [CanvasObject] that can be right-clicked or long-tapped. +type SecondaryTappable interface { + TappedSecondary(*PointEvent) +} + +// Shortcutable describes any [CanvasObject] that can respond to shortcut commands (quit, cut, copy, and paste). +type Shortcutable interface { + TypedShortcut(Shortcut) +} + +// Tabbable describes any object that needs to accept the Tab key presses. +// +// Since: 2.1 +type Tabbable interface { + // AcceptsTab is a hook called by the key press handling logic. + // If it returns true then the Tab key events will be sent using TypedKey. + AcceptsTab() bool +} + +// Tappable describes any [CanvasObject] that can also be tapped. +// This should be implemented by buttons etc that wish to handle pointer interactions. +type Tappable interface { + Tapped(*PointEvent) +} diff --git a/vendor/fyne.io/fyne/v2/clipboard.go b/vendor/fyne.io/fyne/v2/clipboard.go new file mode 100644 index 0000000..fe51b9b --- /dev/null +++ b/vendor/fyne.io/fyne/v2/clipboard.go @@ -0,0 +1,9 @@ +package fyne + +// Clipboard represents the system clipboard interface +type Clipboard interface { + // Content returns the clipboard content + Content() string + // SetContent sets the clipboard content + SetContent(content string) +} diff --git a/vendor/fyne.io/fyne/v2/cloud.go b/vendor/fyne.io/fyne/v2/cloud.go new file mode 100644 index 0000000..2e815bb --- /dev/null +++ b/vendor/fyne.io/fyne/v2/cloud.go @@ -0,0 +1,39 @@ +package fyne + +// CloudProvider specifies the identifying information of a cloud provider. +// This information is mostly used by the [fyne.io/cloud.ShowSettings] user flow. +// +// Since: 2.3 +type CloudProvider interface { + // ProviderDescription returns a more detailed description of this cloud provider. + ProviderDescription() string + // ProviderIcon returns an icon resource that is associated with the given cloud service. + ProviderIcon() Resource + // ProviderName returns the name of this cloud provider, usually the name of the service it uses. + ProviderName() string + + // Cleanup is called when this provider is no longer used and should be disposed. + // This is guaranteed to execute before a new provider is `Setup` + Cleanup(App) + // Setup is called when this provider is being used for the first time. + // Returning an error will exit the cloud setup process, though it can be retried. + Setup(App) error +} + +// CloudProviderPreferences interface defines the functionality that a cloud provider will include if it is capable +// of synchronizing user preferences. +// +// Since: 2.3 +type CloudProviderPreferences interface { + // CloudPreferences returns a preference provider that will sync values to the cloud this provider uses. + CloudPreferences(App) Preferences +} + +// CloudProviderStorage interface defines the functionality that a cloud provider will include if it is capable +// of synchronizing user documents. +// +// Since: 2.3 +type CloudProviderStorage interface { + // CloudStorage returns a storage provider that will sync documents to the cloud this provider uses. + CloudStorage(App) Storage +} diff --git a/vendor/fyne.io/fyne/v2/container.go b/vendor/fyne.io/fyne/v2/container.go new file mode 100644 index 0000000..423bf98 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/container.go @@ -0,0 +1,202 @@ +package fyne + +// Declare conformity to [CanvasObject] +var _ CanvasObject = (*Container)(nil) + +// Container is a [CanvasObject] that contains a collection of child objects. +// The layout of the children is set by the specified Layout. +type Container struct { + size Size // The current size of the Container + position Position // The current position of the Container + Hidden bool // Is this Container hidden + + Layout Layout // The Layout algorithm for arranging child [CanvasObject]s + Objects []CanvasObject // The set of [CanvasObject]s this container holds +} + +// NewContainer returns a new [Container] instance holding the specified [CanvasObject]s. +// +// Deprecated: Use [fyne.io/fyne/v2/container.NewWithoutLayout] to create a container that uses manual layout. +func NewContainer(objects ...CanvasObject) *Container { + return NewContainerWithoutLayout(objects...) +} + +// NewContainerWithoutLayout returns a new [Container] instance holding the specified +// [CanvasObject]s that are manually arranged. +// +// Deprecated: Use [fyne.io/fyne/v2/container.NewWithoutLayout] instead. +func NewContainerWithoutLayout(objects ...CanvasObject) *Container { + ret := &Container{ + Objects: objects, + } + + ret.size = ret.MinSize() + return ret +} + +// NewContainerWithLayout returns a new [Container] instance holding the specified +// [CanvasObject]s which will be laid out according to the specified Layout. +// +// Deprecated: Use [fyne.io/fyne/v2/container.New] instead. +func NewContainerWithLayout(layout Layout, objects ...CanvasObject) *Container { + ret := &Container{ + Objects: objects, + Layout: layout, + } + + ret.size = layout.MinSize(objects) + ret.layout() + return ret +} + +// Add appends the specified object to the items this container manages. +// +// Since: 1.4 +func (c *Container) Add(add CanvasObject) { + if add == nil { + return + } + + c.Objects = append(c.Objects, add) + c.layout() +} + +// AddObject adds another [CanvasObject] to the set this Container holds. +// +// Deprecated: Use [Container.Add] instead. +func (c *Container) AddObject(o CanvasObject) { + c.Add(o) +} + +// Hide sets this container, and all its children, to be not visible. +func (c *Container) Hide() { + if c.Hidden { + return + } + + c.Hidden = true + repaint(c) +} + +// MinSize calculates the minimum size of c. +// This is delegated to the [Container.Layout], if specified, otherwise it will be calculated. +func (c *Container) MinSize() Size { + if c.Layout != nil { + return c.Layout.MinSize(c.Objects) + } + + minSize := NewSize(1, 1) + for _, child := range c.Objects { + minSize = minSize.Max(child.MinSize()) + } + + return minSize +} + +// Move the container (and all its children) to a new position, relative to its parent. +func (c *Container) Move(pos Position) { + c.position = pos + repaint(c) +} + +// Position gets the current position of c relative to its parent. +func (c *Container) Position() Position { + return c.position +} + +// Refresh causes this object to be redrawn in its current state +func (c *Container) Refresh() { + c.layout() + + for _, child := range c.Objects { + child.Refresh() + } + + // this is basically just canvas.Refresh(c) without the package loop + o := CurrentApp().Driver().CanvasForObject(c) + if o == nil { + return + } + o.Refresh(c) +} + +// Remove updates the contents of this container to no longer include the specified object. +// This method is not intended to be used inside a loop, to remove all the elements. +// It is much more efficient to call [Container.RemoveAll) instead. +func (c *Container) Remove(rem CanvasObject) { + if len(c.Objects) == 0 { + return + } + + for i, o := range c.Objects { + if o != rem { + continue + } + copy(c.Objects[i:], c.Objects[i+1:]) + c.Objects[len(c.Objects)-1] = nil + c.Objects = c.Objects[:len(c.Objects)-1] + c.layout() + return + } +} + +// RemoveAll updates the contents of this container to no longer include any objects. +// +// Since: 2.2 +func (c *Container) RemoveAll() { + c.Objects = nil + c.layout() +} + +// Resize sets a new size for c. +func (c *Container) Resize(size Size) { + if c.size == size { + return + } + + c.size = size + c.layout() +} + +// Show sets this container, and all its children, to be visible. +func (c *Container) Show() { + if !c.Hidden { + return + } + + c.Hidden = false +} + +// Size returns the current size c. +func (c *Container) Size() Size { + return c.size +} + +// Visible returns true if the container is currently visible, false otherwise. +func (c *Container) Visible() bool { + return !c.Hidden +} + +func (c *Container) layout() { + if c.Layout == nil { + return + } + + c.Layout.Layout(c.Objects, c.size) +} + +// repaint instructs the containing canvas to redraw, even if nothing changed. +// This method is a duplicate of what is in `canvas/canvas.go` to avoid a dependency loop or public API. +func repaint(obj *Container) { + app := CurrentApp() + if app == nil || app.Driver() == nil { + return + } + + c := app.Driver().CanvasForObject(obj) + if c != nil { + if paint, ok := c.(interface{ SetDirty() }); ok { + paint.SetDirty() + } + } +} diff --git a/vendor/fyne.io/fyne/v2/container/apptabs.go b/vendor/fyne.io/fyne/v2/container/apptabs.go new file mode 100644 index 0000000..6e9fdd0 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/container/apptabs.go @@ -0,0 +1,470 @@ +package container + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" +) + +// Declare conformity with Widget interface. +var _ fyne.Widget = (*AppTabs)(nil) + +// AppTabs container is used to split your application into various different areas identified by tabs. +// The tabs contain text and/or an icon and allow the user to switch between the content specified in each TabItem. +// Each item is represented by a button at the edge of the container. +// +// Since: 1.4 +type AppTabs struct { + widget.BaseWidget + + Items []*TabItem + + // Deprecated: Use `OnSelected func(*TabItem)` instead. + OnChanged func(*TabItem) `json:"-"` + OnSelected func(*TabItem) `json:"-"` + OnUnselected func(*TabItem) `json:"-"` + + current int + location TabLocation + isTransitioning bool + + popUpMenu *widget.PopUpMenu +} + +// NewAppTabs creates a new tab container that allows the user to choose between different areas of an app. +// +// Since: 1.4 +func NewAppTabs(items ...*TabItem) *AppTabs { + tabs := &AppTabs{Items: items} + tabs.BaseWidget.ExtendBaseWidget(tabs) + return tabs +} + +// CreateRenderer is a private method to Fyne which links this widget to its renderer +func (t *AppTabs) CreateRenderer() fyne.WidgetRenderer { + t.BaseWidget.ExtendBaseWidget(t) + th := t.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + r := &appTabsRenderer{ + baseTabsRenderer: baseTabsRenderer{ + bar: &fyne.Container{}, + divider: canvas.NewRectangle(th.Color(theme.ColorNameShadow, v)), + indicator: canvas.NewRectangle(th.Color(theme.ColorNamePrimary, v)), + }, + appTabs: t, + } + r.action = r.buildOverflowTabsButton() + r.tabs = t + + // Initially setup the tab bar to only show one tab, all others will be in overflow. + // When the widget is laid out, and we know the size, the tab bar will be updated to show as many as can fit. + r.updateTabs(1) + r.updateIndicator(false) + r.applyTheme(t) + return r +} + +// Append adds a new TabItem to the end of the tab bar. +func (t *AppTabs) Append(item *TabItem) { + t.SetItems(append(t.Items, item)) +} + +// CurrentTab returns the currently selected TabItem. +// +// Deprecated: Use `AppTabs.Selected() *TabItem` instead. +func (t *AppTabs) CurrentTab() *TabItem { + if t.current < 0 || t.current >= len(t.Items) { + return nil + } + return t.Items[t.current] +} + +// CurrentTabIndex returns the index of the currently selected TabItem. +// +// Deprecated: Use `AppTabs.SelectedIndex() int` instead. +func (t *AppTabs) CurrentTabIndex() int { + return t.SelectedIndex() +} + +// DisableIndex disables the TabItem at the specified index. +// +// Since: 2.3 +func (t *AppTabs) DisableIndex(i int) { + disableIndex(t, i) +} + +// DisableItem disables the specified TabItem. +// +// Since: 2.3 +func (t *AppTabs) DisableItem(item *TabItem) { + disableItem(t, item) +} + +// EnableIndex enables the TabItem at the specified index. +// +// Since: 2.3 +func (t *AppTabs) EnableIndex(i int) { + enableIndex(t, i) +} + +// EnableItem enables the specified TabItem. +// +// Since: 2.3 +func (t *AppTabs) EnableItem(item *TabItem) { + enableItem(t, item) +} + +// ExtendBaseWidget is used by an extending widget to make use of BaseWidget functionality. +// +// Deprecated: Support for extending containers is being removed +func (t *AppTabs) ExtendBaseWidget(wid fyne.Widget) { + t.BaseWidget.ExtendBaseWidget(wid) +} + +// Hide hides the widget. +func (t *AppTabs) Hide() { + if t.popUpMenu != nil { + t.popUpMenu.Hide() + t.popUpMenu = nil + } + t.BaseWidget.Hide() +} + +// MinSize returns the size that this widget should not shrink below +func (t *AppTabs) MinSize() fyne.Size { + t.BaseWidget.ExtendBaseWidget(t) + return t.BaseWidget.MinSize() +} + +// Remove tab by value. +func (t *AppTabs) Remove(item *TabItem) { + removeItem(t, item) + t.Refresh() +} + +// RemoveIndex removes tab by index. +func (t *AppTabs) RemoveIndex(index int) { + removeIndex(t, index) + t.Refresh() +} + +// Select sets the specified TabItem to be selected and its content visible. +func (t *AppTabs) Select(item *TabItem) { + selectItem(t, item) +} + +// SelectIndex sets the TabItem at the specific index to be selected and its content visible. +func (t *AppTabs) SelectIndex(index int) { + selectIndex(t, index) +} + +// SelectTab sets the specified TabItem to be selected and its content visible. +// +// Deprecated: Use `AppTabs.Select(*TabItem)` instead. +func (t *AppTabs) SelectTab(item *TabItem) { + for i, child := range t.Items { + if child == item { + t.SelectTabIndex(i) + return + } + } +} + +// SelectTabIndex sets the TabItem at the specific index to be selected and its content visible. +// +// Deprecated: Use `AppTabs.SelectIndex(int)` instead. +func (t *AppTabs) SelectTabIndex(index int) { + if index < 0 || index >= len(t.Items) || t.current == index { + return + } + t.current = index + t.Refresh() + + if t.OnChanged != nil { + t.OnChanged(t.Items[t.current]) + } +} + +// Selected returns the currently selected TabItem. +func (t *AppTabs) Selected() *TabItem { + return selected(t) +} + +// SelectedIndex returns the index of the currently selected TabItem. +func (t *AppTabs) SelectedIndex() int { + return t.selected() +} + +// SetItems sets the containers items and refreshes. +func (t *AppTabs) SetItems(items []*TabItem) { + setItems(t, items) + t.Refresh() +} + +// SetTabLocation sets the location of the tab bar +func (t *AppTabs) SetTabLocation(l TabLocation) { + t.location = tabsAdjustedLocation(l, t) + t.Refresh() +} + +// Show this widget, if it was previously hidden +func (t *AppTabs) Show() { + t.BaseWidget.Show() + t.SelectIndex(t.current) +} + +func (t *AppTabs) onUnselected() func(*TabItem) { + return t.OnUnselected +} + +func (t *AppTabs) onSelected() func(*TabItem) { + return func(tab *TabItem) { + if f := t.OnChanged; f != nil { + f(tab) + } + if f := t.OnSelected; f != nil { + f(tab) + } + } +} + +func (t *AppTabs) items() []*TabItem { + return t.Items +} + +func (t *AppTabs) selected() int { + if len(t.Items) == 0 { + return -1 + } + return t.current +} + +func (t *AppTabs) setItems(items []*TabItem) { + t.Items = items +} + +func (t *AppTabs) setSelected(selected int) { + t.current = selected +} + +func (t *AppTabs) setTransitioning(transitioning bool) { + t.isTransitioning = transitioning +} + +func (t *AppTabs) tabLocation() TabLocation { + return t.location +} + +func (t *AppTabs) transitioning() bool { + return t.isTransitioning +} + +// Declare conformity with WidgetRenderer interface. +var _ fyne.WidgetRenderer = (*appTabsRenderer)(nil) + +type appTabsRenderer struct { + baseTabsRenderer + appTabs *AppTabs +} + +func (r *appTabsRenderer) Layout(size fyne.Size) { + // Try render as many tabs as will fit, others will appear in the overflow + if len(r.appTabs.Items) == 0 { + r.updateTabs(0) + } else { + for i := len(r.appTabs.Items); i > 0; i-- { + r.updateTabs(i) + barMin := r.bar.MinSize() + if r.appTabs.location == TabLocationLeading || r.appTabs.location == TabLocationTrailing { + if barMin.Height <= size.Height { + // Tab bar is short enough to fit + break + } + } else { + if barMin.Width <= size.Width { + // Tab bar is thin enough to fit + break + } + } + } + } + + r.layout(r.appTabs, size) + r.updateIndicator(r.appTabs.transitioning()) + if r.appTabs.transitioning() { + r.appTabs.setTransitioning(false) + } +} + +func (r *appTabsRenderer) MinSize() fyne.Size { + return r.minSize(r.appTabs) +} + +func (r *appTabsRenderer) Objects() []fyne.CanvasObject { + return r.objects(r.appTabs) +} + +func (r *appTabsRenderer) Refresh() { + r.Layout(r.appTabs.Size()) + + r.refresh(r.appTabs) + + canvas.Refresh(r.appTabs) +} + +func (r *appTabsRenderer) buildOverflowTabsButton() (overflow *widget.Button) { + overflow = &widget.Button{Icon: moreIcon(r.appTabs), Importance: widget.LowImportance, OnTapped: func() { + // Show pop up containing all tabs which did not fit in the tab bar + + itemLen, objLen := len(r.appTabs.Items), len(r.bar.Objects[0].(*fyne.Container).Objects) + items := make([]*fyne.MenuItem, 0, itemLen-objLen) + for i := objLen; i < itemLen; i++ { + index := i // capture + // FIXME MenuItem doesn't support icons (#1752) + // FIXME MenuItem can't show if it is the currently selected tab (#1753) + ti := r.appTabs.Items[i] + mi := fyne.NewMenuItem(ti.Text, func() { + r.appTabs.SelectIndex(index) + if r.appTabs.popUpMenu != nil { + r.appTabs.popUpMenu.Hide() + r.appTabs.popUpMenu = nil + } + }) + if ti.Disabled() { + mi.Disabled = true + } + items = append(items, mi) + } + + r.appTabs.popUpMenu = buildPopUpMenu(r.appTabs, overflow, items) + }} + + return overflow +} + +func (r *appTabsRenderer) buildTabButtons(count int) *fyne.Container { + buttons := &fyne.Container{} + + var iconPos buttonIconPosition + if isMobile(r.tabs) { + cells := count + if cells == 0 { + cells = 1 + } + if r.appTabs.location == TabLocationTop || r.appTabs.location == TabLocationBottom { + buttons.Layout = layout.NewGridLayoutWithColumns(cells) + } else { + buttons.Layout = layout.NewGridLayoutWithRows(cells) + } + iconPos = buttonIconTop + } else if r.appTabs.location == TabLocationLeading || r.appTabs.location == TabLocationTrailing { + buttons.Layout = layout.NewVBoxLayout() + iconPos = buttonIconTop + } else { + buttons.Layout = layout.NewHBoxLayout() + iconPos = buttonIconInline + } + + for i := 0; i < count; i++ { + item := r.appTabs.Items[i] + if item.button == nil { + item.button = &tabButton{ + onTapped: func() { r.appTabs.Select(item) }, + tabs: r.tabs, + } + if item.disabled { + item.button.Disable() + } + } + button := item.button + button.icon = item.Icon + button.iconPosition = iconPos + if i == r.appTabs.current { + button.importance = widget.HighImportance + } else { + button.importance = widget.MediumImportance + } + button.text = item.Text + button.textAlignment = fyne.TextAlignCenter + button.Refresh() + buttons.Objects = append(buttons.Objects, button) + } + return buttons +} + +func (r *appTabsRenderer) updateIndicator(animate bool) { + if len(r.appTabs.Items) == 0 || r.appTabs.current < 0 { + r.indicator.Hide() + return + } + r.indicator.Show() + + var selectedPos fyne.Position + var selectedSize fyne.Size + + buttons := r.bar.Objects[0].(*fyne.Container).Objects + if r.appTabs.current >= len(buttons) { + if a := r.action; a != nil { + selectedPos = a.Position() + selectedSize = a.Size() + } + } else { + selected := buttons[r.appTabs.current] + selectedPos = selected.Position() + selectedSize = selected.Size() + } + + var indicatorPos fyne.Position + var indicatorSize fyne.Size + th := r.appTabs.Theme() + pad := th.Size(theme.SizeNamePadding) + + switch r.appTabs.location { + case TabLocationTop: + indicatorPos = fyne.NewPos(selectedPos.X, r.bar.MinSize().Height) + indicatorSize = fyne.NewSize(selectedSize.Width, pad) + case TabLocationLeading: + indicatorPos = fyne.NewPos(r.bar.MinSize().Width, selectedPos.Y) + indicatorSize = fyne.NewSize(pad, selectedSize.Height) + case TabLocationBottom: + indicatorPos = fyne.NewPos(selectedPos.X, r.bar.Position().Y-pad) + indicatorSize = fyne.NewSize(selectedSize.Width, pad) + case TabLocationTrailing: + indicatorPos = fyne.NewPos(r.bar.Position().X-pad, selectedPos.Y) + indicatorSize = fyne.NewSize(pad, selectedSize.Height) + } + + r.moveIndicator(indicatorPos, indicatorSize, th, animate) +} + +func (r *appTabsRenderer) updateTabs(max int) { + tabCount := len(r.appTabs.Items) + + // Set overflow action + if tabCount <= max { + r.action.Hide() + r.bar.Layout = layout.NewStackLayout() + } else { + tabCount = max + r.action.Show() + + // Set layout of tab bar containing tab buttons and overflow action + if r.appTabs.location == TabLocationLeading || r.appTabs.location == TabLocationTrailing { + r.bar.Layout = layout.NewBorderLayout(nil, r.action, nil, nil) + } else { + r.bar.Layout = layout.NewBorderLayout(nil, nil, nil, r.action) + } + } + + buttons := r.buildTabButtons(tabCount) + + r.bar.Objects = []fyne.CanvasObject{buttons} + if a := r.action; a != nil { + r.bar.Objects = append(r.bar.Objects, a) + } + + r.bar.Refresh() +} diff --git a/vendor/fyne.io/fyne/v2/container/clip.go b/vendor/fyne.io/fyne/v2/container/clip.go new file mode 100644 index 0000000..ceca3eb --- /dev/null +++ b/vendor/fyne.io/fyne/v2/container/clip.go @@ -0,0 +1,70 @@ +package container + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/widget" +) + +// Declare conformity with Widget interface +var _ fyne.Widget = (*Clip)(nil) + +// Clip describes a rectangular region that will clip anything outside its bounds. +// +// Since: 2.7 +type Clip struct { + widget.BaseWidget + Content fyne.CanvasObject +} + +// NewClip returns a new rectangular clipping object. +// +// Since: 2.7 +func NewClip(content fyne.CanvasObject) *Clip { + return &Clip{Content: content} +} + +func (c *Clip) CreateRenderer() fyne.WidgetRenderer { + c.ExtendBaseWidget(c) + return newClipRenderer(c) +} + +// MinSize for a Clip simply returns Size{1, 1} as there is no +// explicit content +func (c *Clip) MinSize() fyne.Size { + c.ExtendBaseWidget(c) + return fyne.NewSize(1, 1) +} + +type clipRenderer struct { + c *Clip + objects []fyne.CanvasObject +} + +func newClipRenderer(c *Clip) *clipRenderer { + return &clipRenderer{c: c, objects: []fyne.CanvasObject{c.Content}} +} + +func (r *clipRenderer) Destroy() { +} + +func (r *clipRenderer) Layout(s fyne.Size) { + o := r.objects[0] + o.Resize(s.Max(o.MinSize())) +} + +func (r *clipRenderer) MinSize() fyne.Size { + return r.objects[0].MinSize() +} + +func (r *clipRenderer) Objects() []fyne.CanvasObject { + return r.objects +} + +func (r *clipRenderer) Refresh() { + r.objects[0] = r.c.Content + r.Layout(r.c.Size()) + r.objects[0].Refresh() +} + +// IsClip marks this widget as clipping. It is on the renderer to avoid a public API addition. +func (r *clipRenderer) IsClip() {} diff --git a/vendor/fyne.io/fyne/v2/container/container.go b/vendor/fyne.io/fyne/v2/container/container.go new file mode 100644 index 0000000..13e881d --- /dev/null +++ b/vendor/fyne.io/fyne/v2/container/container.go @@ -0,0 +1,20 @@ +// Package container provides containers that are used to lay out and organise applications. +package container + +import ( + "fyne.io/fyne/v2" +) + +// New returns a new Container instance holding the specified CanvasObjects which will be laid out according to the specified Layout. +// +// Since: 2.0 +func New(layout fyne.Layout, objects ...fyne.CanvasObject) *fyne.Container { + return &fyne.Container{Layout: layout, Objects: objects} +} + +// NewWithoutLayout returns a new Container instance holding the specified CanvasObjects that are manually arranged. +// +// Since: 2.0 +func NewWithoutLayout(objects ...fyne.CanvasObject) *fyne.Container { + return &fyne.Container{Objects: objects} +} diff --git a/vendor/fyne.io/fyne/v2/container/doctabs.go b/vendor/fyne.io/fyne/v2/container/doctabs.go new file mode 100644 index 0000000..dcdf024 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/container/doctabs.go @@ -0,0 +1,489 @@ +package container + +import ( + "image/color" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" +) + +// Declare conformity with Widget interface. +var _ fyne.Widget = (*DocTabs)(nil) + +// DocTabs container is used to display various pieces of content identified by tabs. +// The tabs contain text and/or an icon and allow the user to switch between the content specified in each TabItem. +// Each item is represented by a button at the edge of the container. +// +// Since: 2.1 +type DocTabs struct { + widget.BaseWidget + + Items []*TabItem + + CreateTab func() *TabItem `json:"-"` + CloseIntercept func(*TabItem) `json:"-"` + OnClosed func(*TabItem) `json:"-"` + OnSelected func(*TabItem) `json:"-"` + OnUnselected func(*TabItem) `json:"-"` + + current int + location TabLocation + isTransitioning bool + + popUpMenu *widget.PopUpMenu +} + +// NewDocTabs creates a new tab container that allows the user to choose between various pieces of content. +// +// Since: 2.1 +func NewDocTabs(items ...*TabItem) *DocTabs { + tabs := &DocTabs{Items: items} + tabs.ExtendBaseWidget(tabs) + return tabs +} + +// Append adds a new TabItem to the end of the tab bar. +func (t *DocTabs) Append(item *TabItem) { + t.SetItems(append(t.Items, item)) +} + +// CreateRenderer is a private method to Fyne which links this widget to its renderer +func (t *DocTabs) CreateRenderer() fyne.WidgetRenderer { + t.ExtendBaseWidget(t) + th := t.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + r := &docTabsRenderer{ + baseTabsRenderer: baseTabsRenderer{ + bar: &fyne.Container{}, + divider: canvas.NewRectangle(th.Color(theme.ColorNameShadow, v)), + indicator: canvas.NewRectangle(th.Color(theme.ColorNamePrimary, v)), + }, + docTabs: t, + scroller: NewScroll(&fyne.Container{}), + } + r.action = r.buildAllTabsButton() + r.create = r.buildCreateTabsButton() + r.tabs = t + + r.box = NewHBox(r.create, r.action) + r.scroller.OnScrolled = func(offset fyne.Position) { + r.updateIndicator(false) + } + r.updateAllTabs() + r.updateCreateTab() + r.updateTabs() + r.updateIndicator(false) + r.applyTheme(t) + return r +} + +// DisableIndex disables the TabItem at the specified index. +// +// Since: 2.3 +func (t *DocTabs) DisableIndex(i int) { + disableIndex(t, i) +} + +// DisableItem disables the specified TabItem. +// +// Since: 2.3 +func (t *DocTabs) DisableItem(item *TabItem) { + disableItem(t, item) +} + +// EnableIndex enables the TabItem at the specified index. +// +// Since: 2.3 +func (t *DocTabs) EnableIndex(i int) { + enableIndex(t, i) +} + +// EnableItem enables the specified TabItem. +// +// Since: 2.3 +func (t *DocTabs) EnableItem(item *TabItem) { + enableItem(t, item) +} + +// Hide hides the widget. +func (t *DocTabs) Hide() { + if t.popUpMenu != nil { + t.popUpMenu.Hide() + t.popUpMenu = nil + } + t.BaseWidget.Hide() +} + +// MinSize returns the size that this widget should not shrink below +func (t *DocTabs) MinSize() fyne.Size { + t.ExtendBaseWidget(t) + return t.BaseWidget.MinSize() +} + +// Remove tab by value. +func (t *DocTabs) Remove(item *TabItem) { + removeItem(t, item) + t.Refresh() +} + +// RemoveIndex removes tab by index. +func (t *DocTabs) RemoveIndex(index int) { + removeIndex(t, index) + t.Refresh() +} + +// Select sets the specified TabItem to be selected and its content visible. +func (t *DocTabs) Select(item *TabItem) { + selectItem(t, item) + t.Refresh() +} + +// SelectIndex sets the TabItem at the specific index to be selected and its content visible. +func (t *DocTabs) SelectIndex(index int) { + selectIndex(t, index) +} + +// Selected returns the currently selected TabItem. +func (t *DocTabs) Selected() *TabItem { + return selected(t) +} + +// SelectedIndex returns the index of the currently selected TabItem. +func (t *DocTabs) SelectedIndex() int { + return t.selected() +} + +// SetItems sets the containers items and refreshes. +func (t *DocTabs) SetItems(items []*TabItem) { + setItems(t, items) + t.Refresh() +} + +// SetTabLocation sets the location of the tab bar +func (t *DocTabs) SetTabLocation(l TabLocation) { + t.location = tabsAdjustedLocation(l, t) + t.Refresh() +} + +// Show this widget, if it was previously hidden +func (t *DocTabs) Show() { + t.BaseWidget.Show() + t.SelectIndex(t.current) +} + +func (t *DocTabs) close(item *TabItem) { + if f := t.CloseIntercept; f != nil { + f(item) + } else { + t.Remove(item) + if f := t.OnClosed; f != nil { + f(item) + } + } +} + +func (t *DocTabs) onUnselected() func(*TabItem) { + return t.OnUnselected +} + +func (t *DocTabs) onSelected() func(*TabItem) { + return t.OnSelected +} + +func (t *DocTabs) items() []*TabItem { + return t.Items +} + +func (t *DocTabs) selected() int { + if len(t.Items) == 0 { + return -1 + } + return t.current +} + +func (t *DocTabs) setItems(items []*TabItem) { + t.Items = items +} + +func (t *DocTabs) setSelected(selected int) { + t.current = selected +} + +func (t *DocTabs) setTransitioning(transitioning bool) { + t.isTransitioning = transitioning +} + +func (t *DocTabs) tabLocation() TabLocation { + return t.location +} + +func (t *DocTabs) transitioning() bool { + return t.isTransitioning +} + +// Declare conformity with WidgetRenderer interface. +var _ fyne.WidgetRenderer = (*docTabsRenderer)(nil) + +type docTabsRenderer struct { + baseTabsRenderer + docTabs *DocTabs + scroller *Scroll + box *fyne.Container + create *widget.Button + lastSelected int +} + +func (r *docTabsRenderer) Layout(size fyne.Size) { + r.updateAllTabs() + r.updateCreateTab() + r.updateTabs() + r.layout(r.docTabs, size) + + // lay out buttons before updating indicator, which is relative to their position + buttons := r.scroller.Content.(*fyne.Container) + buttons.Layout.Layout(buttons.Objects, buttons.Size()) + r.updateIndicator(r.docTabs.transitioning()) + + if r.docTabs.transitioning() { + r.docTabs.setTransitioning(false) + } +} + +func (r *docTabsRenderer) MinSize() fyne.Size { + return r.minSize(r.docTabs) +} + +func (r *docTabsRenderer) Objects() []fyne.CanvasObject { + return r.objects(r.docTabs) +} + +func (r *docTabsRenderer) Refresh() { + r.Layout(r.docTabs.Size()) + + if c := r.docTabs.current; c != r.lastSelected { + if c >= 0 && c < len(r.docTabs.Items) { + r.scrollToSelected() + } + r.lastSelected = c + } + + r.refresh(r.docTabs) + + canvas.Refresh(r.docTabs) +} + +func (r *docTabsRenderer) buildAllTabsButton() (all *widget.Button) { + all = &widget.Button{Importance: widget.LowImportance, OnTapped: func() { + // Show pop up containing all tabs + + items := make([]*fyne.MenuItem, len(r.docTabs.Items)) + for i := 0; i < len(r.docTabs.Items); i++ { + index := i // capture + // FIXME MenuItem doesn't support icons (#1752) + items[i] = fyne.NewMenuItem(r.docTabs.Items[i].Text, func() { + r.docTabs.SelectIndex(index) + if r.docTabs.popUpMenu != nil { + r.docTabs.popUpMenu.Hide() + r.docTabs.popUpMenu = nil + } + }) + items[i].Checked = index == r.docTabs.current + } + + r.docTabs.popUpMenu = buildPopUpMenu(r.docTabs, all, items) + }} + + return all +} + +func (r *docTabsRenderer) buildCreateTabsButton() *widget.Button { + create := widget.NewButton("", func() { + if f := r.docTabs.CreateTab; f != nil { + if tab := f(); tab != nil { + r.docTabs.Append(tab) + r.docTabs.SelectIndex(len(r.docTabs.Items) - 1) + } + } + }) + create.Importance = widget.LowImportance + return create +} + +func (r *docTabsRenderer) buildTabButtons(count int, buttons *fyne.Container) { + buttons.Objects = nil + + var iconPos buttonIconPosition + if r.docTabs.location == TabLocationLeading || r.docTabs.location == TabLocationTrailing { + buttons.Layout = layout.NewVBoxLayout() + iconPos = buttonIconTop + } else { + buttons.Layout = layout.NewHBoxLayout() + iconPos = buttonIconInline + } + + for i := 0; i < count; i++ { + item := r.docTabs.Items[i] + if item.button == nil { + item.button = &tabButton{ + onTapped: func() { r.docTabs.Select(item) }, + onClosed: func() { r.docTabs.close(item) }, + tabs: r.tabs, + } + if item.disabled { + item.button.Disable() + } + } + button := item.button + button.icon = item.Icon + button.iconPosition = iconPos + if i == r.docTabs.current { + button.importance = widget.HighImportance + } else { + button.importance = widget.MediumImportance + } + button.text = item.Text + button.textAlignment = fyne.TextAlignLeading + button.Refresh() + buttons.Objects = append(buttons.Objects, button) + } +} + +func (r *docTabsRenderer) scrollToSelected() { + buttons := r.scroller.Content.(*fyne.Container) + + // https://github.com/fyne-io/fyne/issues/3909 + // very dirty temporary fix to this crash! + if r.docTabs.current < 0 || r.docTabs.current >= len(buttons.Objects) { + return + } + + button := buttons.Objects[r.docTabs.current] + pos := button.Position() + size := button.Size() + offset := r.scroller.Offset + viewport := r.scroller.Size() + if r.docTabs.location == TabLocationLeading || r.docTabs.location == TabLocationTrailing { + if pos.Y < offset.Y { + offset.Y = pos.Y + } else if pos.Y+size.Height > offset.Y+viewport.Height { + offset.Y = pos.Y + size.Height - viewport.Height + } + } else { + if pos.X < offset.X { + offset.X = pos.X + } else if pos.X+size.Width > offset.X+viewport.Width { + offset.X = pos.X + size.Width - viewport.Width + } + } + r.scroller.Offset = offset + r.updateIndicator(false) +} + +func (r *docTabsRenderer) updateIndicator(animate bool) { + th := r.docTabs.Theme() + if r.docTabs.current < 0 { + r.indicator.FillColor = color.Transparent + r.moveIndicator(fyne.NewPos(0, 0), fyne.NewSize(0, 0), th, animate) + return + } + + var selectedPos fyne.Position + var selectedSize fyne.Size + + buttons := r.scroller.Content.(*fyne.Container).Objects + + if r.docTabs.current >= len(buttons) { + if a := r.action; a != nil { + selectedPos = a.Position() + selectedSize = a.Size() + minSize := a.MinSize() + if minSize.Width > selectedSize.Width { + selectedSize = minSize + } + } + } else { + selected := buttons[r.docTabs.current] + selectedPos = selected.Position() + selectedSize = selected.Size() + minSize := selected.MinSize() + if minSize.Width > selectedSize.Width { + selectedSize = minSize + } + } + + scrollOffset := r.scroller.Offset + scrollSize := r.scroller.Size() + + var indicatorPos fyne.Position + var indicatorSize fyne.Size + pad := th.Size(theme.SizeNamePadding) + + switch r.docTabs.location { + case TabLocationTop: + indicatorPos = fyne.NewPos(selectedPos.X-scrollOffset.X, r.bar.MinSize().Height) + indicatorSize = fyne.NewSize(fyne.Min(selectedSize.Width, scrollSize.Width-indicatorPos.X), pad) + case TabLocationLeading: + indicatorPos = fyne.NewPos(r.bar.MinSize().Width, selectedPos.Y-scrollOffset.Y) + indicatorSize = fyne.NewSize(pad, fyne.Min(selectedSize.Height, scrollSize.Height-indicatorPos.Y)) + case TabLocationBottom: + indicatorPos = fyne.NewPos(selectedPos.X-scrollOffset.X, r.bar.Position().Y-pad) + indicatorSize = fyne.NewSize(fyne.Min(selectedSize.Width, scrollSize.Width-indicatorPos.X), pad) + case TabLocationTrailing: + indicatorPos = fyne.NewPos(r.bar.Position().X-pad, selectedPos.Y-scrollOffset.Y) + indicatorSize = fyne.NewSize(pad, fyne.Min(selectedSize.Height, scrollSize.Height-indicatorPos.Y)) + } + + if indicatorPos.X < 0 { + indicatorSize.Width = indicatorSize.Width + indicatorPos.X + indicatorPos.X = 0 + } + if indicatorPos.Y < 0 { + indicatorSize.Height = indicatorSize.Height + indicatorPos.Y + indicatorPos.Y = 0 + } + if indicatorSize.Width < 0 || indicatorSize.Height < 0 { + r.indicator.FillColor = color.Transparent + r.indicator.Refresh() + return + } + + r.moveIndicator(indicatorPos, indicatorSize, th, animate) +} + +func (r *docTabsRenderer) updateAllTabs() { + if len(r.docTabs.Items) > 0 { + r.action.Show() + } else { + r.action.Hide() + } +} + +func (r *docTabsRenderer) updateCreateTab() { + if r.docTabs.CreateTab != nil { + r.create.SetIcon(theme.ContentAddIcon()) + r.create.Show() + } else { + r.create.Hide() + } +} + +func (r *docTabsRenderer) updateTabs() { + tabCount := len(r.docTabs.Items) + r.buildTabButtons(tabCount, r.scroller.Content.(*fyne.Container)) + + // Set layout of tab bar containing tab buttons and overflow action + if r.docTabs.location == TabLocationLeading || r.docTabs.location == TabLocationTrailing { + r.bar.Layout = layout.NewBorderLayout(nil, r.box, nil, nil) + r.scroller.Direction = ScrollVerticalOnly + } else { + r.bar.Layout = layout.NewBorderLayout(nil, nil, nil, r.box) + r.scroller.Direction = ScrollHorizontalOnly + } + + r.bar.Objects = []fyne.CanvasObject{r.scroller, r.box} + r.bar.Refresh() +} diff --git a/vendor/fyne.io/fyne/v2/container/innerwindow.go b/vendor/fyne.io/fyne/v2/container/innerwindow.go new file mode 100644 index 0000000..57d02f0 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/container/innerwindow.go @@ -0,0 +1,444 @@ +package container + +import ( + "image/color" + "runtime" + "strings" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + intWidget "fyne.io/fyne/v2/internal/widget" + "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" +) + +type titleBarButtonMode int + +const ( + modeClose titleBarButtonMode = iota + modeMinimize + modeMaximize + modeIcon +) + +var _ fyne.Widget = (*InnerWindow)(nil) + +// InnerWindow defines a container that wraps content in a window border - that can then be placed inside +// a regular container/canvas. +// +// Since: 2.5 +type InnerWindow struct { + widget.BaseWidget + + CloseIntercept func() `json:"-"` + OnDragged, OnResized func(*fyne.DragEvent) `json:"-"` + OnMinimized, OnMaximized, OnTappedBar, OnTappedIcon func() `json:"-"` + Icon fyne.Resource + + // Alignment allows an inner window to specify if the buttons should be on the left + // (`ButtonAlignLeading`) or right of the window border. + // + // Since: 2.6 + Alignment widget.ButtonAlign + + title string + content *fyne.Container + maximized bool +} + +// NewInnerWindow creates a new window border around the given `content`, displaying the `title` along the top. +// This will behave like a normal contain and will probably want to be added to a `MultipleWindows` parent. +// +// Since: 2.5 +func NewInnerWindow(title string, content fyne.CanvasObject) *InnerWindow { + w := &InnerWindow{title: title, content: NewPadded(content)} + w.ExtendBaseWidget(w) + return w +} + +func (w *InnerWindow) Close() { + w.Hide() +} + +func (w *InnerWindow) CreateRenderer() fyne.WidgetRenderer { + w.ExtendBaseWidget(w) + th := w.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + min := newBorderButton(theme.WindowMinimizeIcon(), modeMinimize, th, w.OnMinimized) + if w.OnMinimized == nil { + min.Disable() + } + max := newBorderButton(theme.WindowMaximizeIcon(), modeMaximize, th, w.OnMaximized) + if w.OnMaximized == nil { + max.Disable() + } + + close := newBorderButton(theme.WindowCloseIcon(), modeClose, th, func() { + if f := w.CloseIntercept; f != nil { + f() + } else { + w.Close() + } + }) + buttons := NewCenter(NewHBox(close, min, max)) + + borderIcon := newBorderButton(w.Icon, modeIcon, th, func() { + if f := w.OnTappedIcon; f != nil { + f() + } + }) + if w.OnTappedIcon == nil { + borderIcon.Disable() + } + + if w.Icon == nil { + borderIcon.Hide() + } + title := newDraggableLabel(w.title, w) + title.Truncation = fyne.TextTruncateEllipsis + + height := w.Theme().Size(theme.SizeNameWindowTitleBarHeight) + off := (height - title.labelMinSize().Height) / 2 + barMid := New(layout.NewCustomPaddedLayout(off, 0, 0, 0), title) + if w.buttonPosition() == widget.ButtonAlignTrailing { + buttons = NewCenter(NewHBox(min, max, close)) + } + + bg := canvas.NewRectangle(th.Color(theme.ColorNameOverlayBackground, v)) + contentBG := canvas.NewRectangle(th.Color(theme.ColorNameBackground, v)) + corner := newDraggableCorner(w) + bar := New(&titleBarLayout{buttons: buttons, icon: borderIcon, title: barMid, win: w}, + buttons, borderIcon, barMid) + + if w.content == nil { + w.content = NewPadded(canvas.NewRectangle(color.Transparent)) + } + objects := []fyne.CanvasObject{bg, contentBG, bar, w.content, corner} + r := &innerWindowRenderer{ + ShadowingRenderer: intWidget.NewShadowingRenderer(objects, intWidget.DialogLevel), + win: w, bar: bar, buttonBox: buttons, buttons: []*borderButton{close, min, max}, bg: bg, + corner: corner, contentBG: contentBG, icon: borderIcon, + } + r.Layout(w.Size()) + return r +} + +func (w *InnerWindow) SetContent(obj fyne.CanvasObject) { + w.content.Objects[0] = obj + + w.content.Refresh() +} + +// SetMaximized tells the window if the maximized state should be set or not. +// +// Since: 2.6 +func (w *InnerWindow) SetMaximized(max bool) { + w.maximized = max + w.Refresh() +} + +func (w *InnerWindow) SetPadded(pad bool) { + if pad { + w.content.Layout = layout.NewPaddedLayout() + } else { + w.content.Layout = layout.NewStackLayout() + } + w.content.Refresh() +} + +func (w *InnerWindow) SetTitle(title string) { + w.title = title + w.Refresh() +} + +func (w *InnerWindow) buttonPosition() widget.ButtonAlign { + if w.Alignment != widget.ButtonAlignCenter { + return w.Alignment + } + + if runtime.GOOS == "windows" || runtime.GOOS == "linux" || strings.Contains(runtime.GOOS, "bsd") { + return widget.ButtonAlignTrailing + } + // macOS + return widget.ButtonAlignLeading +} + +var _ fyne.WidgetRenderer = (*innerWindowRenderer)(nil) + +type innerWindowRenderer struct { + *intWidget.ShadowingRenderer + + win *InnerWindow + bar, buttonBox *fyne.Container + buttons []*borderButton + icon *borderButton + bg, contentBG *canvas.Rectangle + corner fyne.CanvasObject +} + +func (i *innerWindowRenderer) Layout(size fyne.Size) { + th := i.win.Theme() + pad := th.Size(theme.SizeNamePadding) + + i.LayoutShadow(size, fyne.Position{}) + i.bg.Resize(size) + + barHeight := i.win.Theme().Size(theme.SizeNameWindowTitleBarHeight) + i.bar.Move(fyne.NewPos(pad, 0)) + i.bar.Resize(fyne.NewSize(size.Width-pad*2, barHeight)) + + innerPos := fyne.NewPos(pad, barHeight) + innerSize := fyne.NewSize(size.Width-pad*2, size.Height-pad-barHeight) + i.contentBG.Move(innerPos) + i.contentBG.Resize(innerSize) + i.win.content.Move(innerPos) + i.win.content.Resize(innerSize) + + cornerSize := i.corner.MinSize() + i.corner.Move(fyne.NewPos(size.Components()).Subtract(cornerSize).AddXY(1, 1)) + i.corner.Resize(cornerSize) +} + +func (i *innerWindowRenderer) MinSize() fyne.Size { + th := i.win.Theme() + pad := th.Size(theme.SizeNamePadding) + contentMin := i.win.content.MinSize() + barHeight := th.Size(theme.SizeNameWindowTitleBarHeight) + + innerWidth := fyne.Max(i.bar.MinSize().Width, contentMin.Width) + + return fyne.NewSize(innerWidth+pad*2, contentMin.Height+pad+barHeight) +} + +func (i *innerWindowRenderer) Refresh() { + th := i.win.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + i.bg.FillColor = th.Color(theme.ColorNameOverlayBackground, v) + i.bg.Refresh() + i.contentBG.FillColor = th.Color(theme.ColorNameBackground, v) + i.contentBG.Refresh() + + if i.win.buttonPosition() == widget.ButtonAlignTrailing { + i.buttonBox.Objects[0].(*fyne.Container).Objects = []fyne.CanvasObject{i.buttons[1], i.buttons[2], i.buttons[0]} + } else { + i.buttonBox.Objects[0].(*fyne.Container).Objects = []fyne.CanvasObject{i.buttons[0], i.buttons[1], i.buttons[2]} + } + for _, b := range i.buttons { + b.setTheme(th) + } + i.bar.Refresh() + + if i.win.OnMinimized == nil { + i.buttons[1].Disable() + } else { + i.buttons[1].SetOnTapped(i.win.OnMinimized) + i.buttons[1].Enable() + } + + max := i.buttons[2] + if i.win.OnMaximized == nil { + i.buttons[2].Disable() + } else { + max.SetOnTapped(i.win.OnMaximized) + max.Enable() + } + if i.win.maximized { + max.b.SetIcon(theme.ViewRestoreIcon()) + } else { + max.b.SetIcon(theme.WindowMaximizeIcon()) + } + + title := i.bar.Objects[2].(*fyne.Container).Objects[0].(*draggableLabel) + title.SetText(i.win.title) + i.ShadowingRenderer.RefreshShadow() + if i.win.OnTappedIcon == nil { + i.icon.Disable() + } else { + i.icon.Enable() + } + if i.win.Icon != nil { + i.icon.b.SetIcon(i.win.Icon) + i.icon.Show() + } else { + i.icon.Hide() + } +} + +type draggableLabel struct { + widget.Label + win *InnerWindow +} + +func newDraggableLabel(title string, win *InnerWindow) *draggableLabel { + d := &draggableLabel{win: win} + d.ExtendBaseWidget(d) + d.Text = title + return d +} + +func (d *draggableLabel) Dragged(ev *fyne.DragEvent) { + if f := d.win.OnDragged; f != nil { + f(ev) + } +} + +func (d *draggableLabel) DragEnd() { +} + +func (d *draggableLabel) MinSize() fyne.Size { + width := d.Label.MinSize().Width + height := d.Label.Theme().Size(theme.SizeNameWindowButtonHeight) + return fyne.NewSize(width, height) +} + +func (d *draggableLabel) Tapped(_ *fyne.PointEvent) { + if f := d.win.OnTappedBar; f != nil { + f() + } +} + +func (d *draggableLabel) labelMinSize() fyne.Size { + return d.Label.MinSize() +} + +type draggableCorner struct { + widget.BaseWidget + win *InnerWindow +} + +func newDraggableCorner(w *InnerWindow) *draggableCorner { + d := &draggableCorner{win: w} + d.ExtendBaseWidget(d) + return d +} + +func (c *draggableCorner) CreateRenderer() fyne.WidgetRenderer { + prop := canvas.NewImageFromResource(fyne.CurrentApp().Settings().Theme().Icon(theme.IconNameDragCornerIndicator)) + prop.SetMinSize(fyne.NewSquareSize(16)) + return widget.NewSimpleRenderer(prop) +} + +func (c *draggableCorner) Dragged(ev *fyne.DragEvent) { + if f := c.win.OnResized; f != nil { + c.win.OnResized(ev) + } +} + +func (c *draggableCorner) DragEnd() { +} + +type borderButton struct { + widget.BaseWidget + + b *widget.Button + c *ThemeOverride + mode titleBarButtonMode +} + +func newBorderButton(icon fyne.Resource, mode titleBarButtonMode, th fyne.Theme, fn func()) *borderButton { + buttonImportance := widget.MediumImportance + if mode == modeIcon { + buttonImportance = widget.LowImportance + } + b := &widget.Button{Icon: icon, Importance: buttonImportance, OnTapped: fn} + c := NewThemeOverride(b, &buttonTheme{Theme: th, mode: mode}) + + ret := &borderButton{b: b, c: c, mode: mode} + ret.ExtendBaseWidget(ret) + return ret +} + +func (b *borderButton) CreateRenderer() fyne.WidgetRenderer { + return widget.NewSimpleRenderer(b.c) +} + +func (b *borderButton) Disable() { + b.b.Disable() +} + +func (b *borderButton) Enable() { + b.b.Enable() +} + +func (b *borderButton) SetOnTapped(fn func()) { + b.b.OnTapped = fn +} + +func (b *borderButton) MinSize() fyne.Size { + height := b.Theme().Size(theme.SizeNameWindowButtonHeight) + return fyne.NewSquareSize(height) +} + +func (b *borderButton) setTheme(th fyne.Theme) { + b.c.Theme = &buttonTheme{Theme: th, mode: b.mode} +} + +type buttonTheme struct { + fyne.Theme + mode titleBarButtonMode +} + +func (b *buttonTheme) Color(n fyne.ThemeColorName, v fyne.ThemeVariant) color.Color { + switch n { + case theme.ColorNameHover: + if b.mode == modeClose { + n = theme.ColorNameError + } + } + return b.Theme.Color(n, v) +} + +func (b *buttonTheme) Size(n fyne.ThemeSizeName) float32 { + switch n { + case theme.SizeNameInputRadius: + if b.mode == modeIcon { + return 0 + } + n = theme.SizeNameWindowButtonRadius + case theme.SizeNameInlineIcon: + n = theme.SizeNameWindowButtonIcon + } + + return b.Theme.Size(n) +} + +type titleBarLayout struct { + win *InnerWindow + buttons, icon, title fyne.CanvasObject +} + +func (t *titleBarLayout) Layout(_ []fyne.CanvasObject, s fyne.Size) { + buttonMinWidth := t.buttons.MinSize().Width + t.buttons.Resize(fyne.NewSize(buttonMinWidth, s.Height)) + t.icon.Resize(fyne.NewSquareSize(s.Height)) + usedWidth := buttonMinWidth + if t.icon.Visible() { + usedWidth += s.Height + } + t.title.Resize(fyne.NewSize(s.Width-usedWidth, s.Height)) + + if t.win.buttonPosition() == widget.ButtonAlignTrailing { + t.buttons.Move(fyne.NewPos(s.Width-buttonMinWidth, 0)) + t.icon.Move(fyne.Position{}) + if t.icon.Visible() { + t.title.Move(fyne.NewPos(s.Height, 0)) + } else { + t.title.Move(fyne.Position{}) + } + } else { + t.buttons.Move(fyne.NewPos(0, 0)) + t.icon.Move(fyne.NewPos(s.Width-s.Height, 0)) + t.title.Move(fyne.NewPos(buttonMinWidth, 0)) + } +} + +func (t *titleBarLayout) MinSize(_ []fyne.CanvasObject) fyne.Size { + buttonMin := t.buttons.MinSize() + iconMin := t.icon.MinSize() + titleMin := t.title.MinSize() // can truncate + + return fyne.NewSize(buttonMin.Width+iconMin.Width+titleMin.Width, + fyne.Max(fyne.Max(buttonMin.Height, iconMin.Height), titleMin.Height)) +} diff --git a/vendor/fyne.io/fyne/v2/container/layouts.go b/vendor/fyne.io/fyne/v2/container/layouts.go new file mode 100644 index 0000000..1e84463 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/container/layouts.go @@ -0,0 +1,124 @@ +package container // import "fyne.io/fyne/v2/container" + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal" + "fyne.io/fyne/v2/layout" +) + +// NewAdaptiveGrid creates a new container with the specified objects and using the grid layout. +// When in a horizontal arrangement the rowcols parameter will specify the column count, when in vertical +// it will specify the rows. On mobile this will dynamically refresh when device is rotated. +// +// Since: 1.4 +func NewAdaptiveGrid(rowcols int, objects ...fyne.CanvasObject) *fyne.Container { + return New(layout.NewAdaptiveGridLayout(rowcols), objects...) +} + +// NewBorder creates a new container with the specified objects and using the border layout. +// The top, bottom, left and right parameters specify the items that should be placed around edges. +// Nil can be used to an edge if it should not be filled. +// Passed objects not assigned to any edge (parameters 5 onwards) will be used to fill the space +// remaining in the middle. +// Parameters 6 onwards will be stacked over the middle content in the specified order as a Stack container. +// +// Since: 1.4 +func NewBorder(top, bottom, left, right fyne.CanvasObject, objects ...fyne.CanvasObject) *fyne.Container { + all := objects + if top != nil { + all = append(all, top) + } + if bottom != nil { + all = append(all, bottom) + } + if left != nil { + all = append(all, left) + } + if right != nil { + all = append(all, right) + } + + if len(objects) == 1 && objects[0] == nil { + internal.LogHint("Border layout requires only 4 parameters, optional items cannot be nil") + all = all[1:] + } + return New(layout.NewBorderLayout(top, bottom, left, right), all...) +} + +// NewCenter creates a new container with the specified objects centered in the available space. +// +// Since: 1.4 +func NewCenter(objects ...fyne.CanvasObject) *fyne.Container { + return New(layout.NewCenterLayout(), objects...) +} + +// NewGridWithColumns creates a new container with the specified objects and using the grid layout with +// a specified number of columns. The number of rows will depend on how many children are in the container. +// +// Since: 1.4 +func NewGridWithColumns(cols int, objects ...fyne.CanvasObject) *fyne.Container { + return New(layout.NewGridLayoutWithColumns(cols), objects...) +} + +// NewGridWithRows creates a new container with the specified objects and using the grid layout with +// a specified number of rows. The number of columns will depend on how many children are in the container. +// +// Since: 1.4 +func NewGridWithRows(rows int, objects ...fyne.CanvasObject) *fyne.Container { + return New(layout.NewGridLayoutWithRows(rows), objects...) +} + +// NewGridWrap creates a new container with the specified objects and using the gridwrap layout. +// Every element will be resized to the size parameter and the content will arrange along a row and flow to a +// new row if the elements don't fit. +// +// Since: 1.4 +func NewGridWrap(size fyne.Size, objects ...fyne.CanvasObject) *fyne.Container { + return New(layout.NewGridWrapLayout(size), objects...) +} + +// NewHBox creates a new container with the specified objects and using the HBox layout. +// The objects will be placed in the container from left to right and always displayed +// at their horizontal MinSize. Use a different layout if the objects are intended +// to be larger then their horizontal MinSize. +// +// Since: 1.4 +func NewHBox(objects ...fyne.CanvasObject) *fyne.Container { + return New(layout.NewHBoxLayout(), objects...) +} + +// NewMax creates a new container with the specified objects filling the available space. +// +// Since: 1.4 +// +// Deprecated: Use container.NewStack() instead. +func NewMax(objects ...fyne.CanvasObject) *fyne.Container { + return NewStack(objects...) +} + +// NewPadded creates a new container with the specified objects inset by standard padding size. +// +// Since: 1.4 +func NewPadded(objects ...fyne.CanvasObject) *fyne.Container { + return New(layout.NewPaddedLayout(), objects...) +} + +// NewStack returns a new container that stacks objects on top of each other. +// Objects at the end of the container will be stacked on top of objects before. +// Having only a single object has no impact as CanvasObjects will +// fill the available space even without a Stack. +// +// Since: 2.4 +func NewStack(objects ...fyne.CanvasObject) *fyne.Container { + return New(layout.NewStackLayout(), objects...) +} + +// NewVBox creates a new container with the specified objects and using the VBox layout. +// The objects will be stacked in the container from top to bottom and always displayed +// at their vertical MinSize. Use a different layout if the objects are intended +// to be larger then their vertical MinSize. +// +// Since: 1.4 +func NewVBox(objects ...fyne.CanvasObject) *fyne.Container { + return New(layout.NewVBoxLayout(), objects...) +} diff --git a/vendor/fyne.io/fyne/v2/container/multiplewindows.go b/vendor/fyne.io/fyne/v2/container/multiplewindows.go new file mode 100644 index 0000000..eb3bec5 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/container/multiplewindows.go @@ -0,0 +1,104 @@ +package container + +import ( + "fyne.io/fyne/v2" + intWidget "fyne.io/fyne/v2/internal/widget" + "fyne.io/fyne/v2/widget" +) + +// MultipleWindows is a container that handles multiple `InnerWindow` containers. +// Each inner window can be dragged, resized and the stacking will change when the title bar is tapped. +// +// Since: 2.5 +type MultipleWindows struct { + widget.BaseWidget + + Windows []*InnerWindow + + content *fyne.Container +} + +// NewMultipleWindows creates a new `MultipleWindows` container to manage many inner windows. +// The initial window list is passed optionally to this constructor function. +// You can add new more windows to this container by calling `Add` or updating the `Windows` +// field and calling `Refresh`. +// +// Since: 2.5 +func NewMultipleWindows(wins ...*InnerWindow) *MultipleWindows { + m := &MultipleWindows{Windows: wins} + m.ExtendBaseWidget(m) + return m +} + +func (m *MultipleWindows) Add(w *InnerWindow) { + m.Windows = append(m.Windows, w) + m.refreshChildren() +} + +func (m *MultipleWindows) CreateRenderer() fyne.WidgetRenderer { + m.content = New(&multiWinLayout{}) + m.refreshChildren() + return widget.NewSimpleRenderer(intWidget.NewScroll(m.content)) +} + +func (m *MultipleWindows) Refresh() { + m.refreshChildren() + // m.BaseWidget.Refresh() +} + +func (m *MultipleWindows) raise(w *InnerWindow) { + id := -1 + for i, ww := range m.Windows { + if ww == w { + id = i + break + } + } + if id == -1 { + return + } + + windows := append(m.Windows[:id], m.Windows[id+1:]...) + m.Windows = append(windows, w) + m.refreshChildren() +} + +func (m *MultipleWindows) refreshChildren() { + if m.content == nil { + return + } + + objs := make([]fyne.CanvasObject, len(m.Windows)) + for i, w := range m.Windows { + objs[i] = w + + m.setupChild(w) + } + m.content.Objects = objs + m.content.Refresh() +} + +func (m *MultipleWindows) setupChild(w *InnerWindow) { + w.OnDragged = func(ev *fyne.DragEvent) { + w.Move(w.Position().Add(ev.Dragged)) + } + w.OnResized = func(ev *fyne.DragEvent) { + size := w.Size().Add(ev.Dragged) + w.Resize(size.Max(w.MinSize())) + } + w.OnTappedBar = func() { + m.raise(w) + } +} + +type multiWinLayout struct{} + +func (m *multiWinLayout) Layout(objects []fyne.CanvasObject, _ fyne.Size) { + for _, w := range objects { // update the windows so they have real size + w.Resize(w.MinSize().Max(w.Size())) + } +} + +func (m *multiWinLayout) MinSize(_ []fyne.CanvasObject) fyne.Size { + return fyne.Size{} +} diff --git a/vendor/fyne.io/fyne/v2/container/navigation.go b/vendor/fyne.io/fyne/v2/container/navigation.go new file mode 100644 index 0000000..8dc44c5 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/container/navigation.go @@ -0,0 +1,215 @@ +package container + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" +) + +// Navigation container is used to provide your application with a control bar and an area for content objects. +// Objects can be any CanvasObject, and only the most recent one will be visible. +// +// Since: 2.7 +type Navigation struct { + widget.BaseWidget + + Root fyne.CanvasObject + Title string + OnBack func() + OnForward func() + + level int + stack fyne.Container + titles []string +} + +// NewNavigation creates a new navigation container with a given root object. +// +// Since: 2.7 +func NewNavigation(root fyne.CanvasObject) *Navigation { + return NewNavigationWithTitle(root, "") +} + +// NewNavigationWithTitle creates a new navigation container with a given root object and a default title. +// +// Since: 2.7 +func NewNavigationWithTitle(root fyne.CanvasObject, s string) *Navigation { + var nav *Navigation + nav = &Navigation{ + Root: root, + Title: s, + OnBack: func() { _ = nav.Back() }, + OnForward: func() { _ = nav.Forward() }, + } + return nav +} + +// Push puts the given object on top of the navigation stack and hides the object below. +// +// Since: 2.7 +func (nav *Navigation) Push(obj fyne.CanvasObject) { + nav.PushWithTitle(obj, nav.Title) +} + +// PushWithTitle puts the given CanvasObject on top, hides the object below, and uses the given title as label text. +// +// Since: 2.7 +func (nav *Navigation) PushWithTitle(obj fyne.CanvasObject, s string) { + obj.Show() + objs := nav.stack.Objects[:nav.level] + if len(objs) > 0 { + objs[len(objs)-1].Hide() + } + nav.stack.Objects = append(objs, obj) + nav.titles = append(nav.titles[:nav.level], s) + nav.level++ + nav.Refresh() +} + +// Back returns the top level CanvasObject, adjusts the title accordingly, and disabled the back button +// when no more objects are left to go back to. +// +// Since: 2.7 +func (nav *Navigation) Back() fyne.CanvasObject { + if nav.level == 0 || nav.level == 1 && nav.Root != nil { + return nil + } + + objs := nav.stack.Objects + objs[nav.level-1].Hide() + if nav.level > 1 { + objs[nav.level-2].Show() + } + + nav.level-- + nav.Refresh() + + return objs[nav.level] +} + +// Forward shows the next object in the stack again. +// +// Since: 2.7 +func (nav *Navigation) Forward() fyne.CanvasObject { + if nav.level >= len(nav.stack.Objects) { + return nil + } + + nav.stack.Objects[nav.level-1].Hide() + nav.stack.Objects[nav.level].Show() + nav.level++ + + return nav.stack.Objects[nav.level-1] +} + +// SetTitle changes the root navigation title shown by default. +// +// Since: 2.7 +func (nav *Navigation) SetTitle(s string) { + nav.Title = s + nav.Refresh() +} + +// SetCurrentTitle changes the navigation title for the current level. +// +// Since: 2.7 +func (nav *Navigation) SetCurrentTitle(s string) { + if nav.level > 1 && nav.level-1 < len(nav.titles) { + nav.titles[nav.level-1] = s + nav.Refresh() + } +} + +func (nav *Navigation) setup() { + objs := []fyne.CanvasObject{} + titles := []string{} + if nav.Root != nil { + objs = append(objs, nav.Root) + titles = append(titles, nav.Title) + } + nav.level = len(objs) + nav.stack.Layout = layout.NewStackLayout() + nav.stack.Objects = objs + nav.titles = titles + nav.ExtendBaseWidget(nav) +} + +var _ fyne.WidgetRenderer = (*navigatorRenderer)(nil) + +type navigatorRenderer struct { + nav *Navigation + back widget.Button + forward widget.Button + title widget.Label + object fyne.CanvasObject +} + +func (nav *Navigation) CreateRenderer() fyne.WidgetRenderer { + r := &navigatorRenderer{ + nav: nav, + title: widget.Label{ + Text: nav.Title, + Alignment: fyne.TextAlignCenter, + }, + back: widget.Button{ + Icon: theme.NavigateBackIcon(), + OnTapped: nav.OnBack, + }, + forward: widget.Button{ + Icon: theme.NavigateNextIcon(), + OnTapped: nav.OnForward, + }, + } + r.back.Disable() + r.forward.Disable() + + nav.setup() + + r.object = NewBorder( + NewStack(NewHBox(&r.back, layout.NewSpacer(), &r.forward), &r.title), + nil, + nil, + nil, + &nav.stack, + ) + + return r +} + +func (r *navigatorRenderer) Destroy() { +} + +func (r *navigatorRenderer) Layout(s fyne.Size) { + r.object.Resize(s) +} + +func (r *navigatorRenderer) MinSize() fyne.Size { + return r.object.MinSize() +} + +func (r *navigatorRenderer) Objects() []fyne.CanvasObject { + return []fyne.CanvasObject{r.object} +} + +func (r *navigatorRenderer) Refresh() { + if r.nav.level < 1 || r.nav.level == 1 && r.nav.Root != nil { + r.back.Disable() + } else { + r.back.Enable() + } + + if r.nav.level == len(r.nav.stack.Objects) { + r.forward.Disable() + } else { + r.forward.Enable() + } + + if r.nav.level-1 >= 0 && r.nav.level-1 < len(r.nav.titles) { + r.title.Text = r.nav.titles[r.nav.level-1] + } else { + r.title.Text = r.nav.Title + } + + r.object.Refresh() +} diff --git a/vendor/fyne.io/fyne/v2/container/scroll.go b/vendor/fyne.io/fyne/v2/container/scroll.go new file mode 100644 index 0000000..dda8aa6 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/container/scroll.go @@ -0,0 +1,55 @@ +package container + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/widget" +) + +// Scroll defines a container that is smaller than the Content. +// The Offset is used to determine the position of the child widgets within the container. +// +// Since: 1.4 +type Scroll = widget.Scroll + +// ScrollDirection represents the directions in which a Scroll container can scroll its child content. +// +// Since: 1.4 +type ScrollDirection = fyne.ScrollDirection + +// Constants for valid values of ScrollDirection. +const ( + // ScrollBoth supports horizontal and vertical scrolling. + ScrollBoth ScrollDirection = fyne.ScrollBoth + // ScrollHorizontalOnly specifies the scrolling should only happen left to right. + ScrollHorizontalOnly = fyne.ScrollHorizontalOnly + // ScrollVerticalOnly specifies the scrolling should only happen top to bottom. + ScrollVerticalOnly = fyne.ScrollVerticalOnly + // ScrollNone turns off scrolling for this container. + // + // Since: 2.1 + ScrollNone = fyne.ScrollNone +) + +// NewScroll creates a scrollable parent wrapping the specified content. +// Note that this may cause the MinSize to be smaller than that of the passed object. +// +// Since: 1.4 +func NewScroll(content fyne.CanvasObject) *Scroll { + return widget.NewScroll(content) +} + +// NewHScroll create a scrollable parent wrapping the specified content. +// Note that this may cause the MinSize.Width to be smaller than that of the passed object. +// +// Since: 1.4 +func NewHScroll(content fyne.CanvasObject) *Scroll { + return widget.NewHScroll(content) +} + +// NewVScroll a scrollable parent wrapping the specified content. +// Note that this may cause the MinSize.Height to be smaller than that of the passed object. +// +// Since: 1.4 +func NewVScroll(content fyne.CanvasObject) *Scroll { + return widget.NewVScroll(content) +} diff --git a/vendor/fyne.io/fyne/v2/container/split.go b/vendor/fyne.io/fyne/v2/container/split.go new file mode 100644 index 0000000..31be62e --- /dev/null +++ b/vendor/fyne.io/fyne/v2/container/split.go @@ -0,0 +1,420 @@ +package container + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/driver/desktop" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" +) + +// Declare conformity with CanvasObject interface +var _ fyne.CanvasObject = (*Split)(nil) + +// Split defines a container whose size is split between two children. +// +// Since: 1.4 +type Split struct { + widget.BaseWidget + Offset float64 + Horizontal bool + Leading fyne.CanvasObject + Trailing fyne.CanvasObject + + // to communicate to the renderer that the next refresh + // is just an offset update (ie a resize and move only) + // cleared by renderer in Refresh() + offsetUpdated bool +} + +// NewHSplit creates a horizontally arranged container with the specified leading and trailing elements. +// A vertical split bar that can be dragged will be added between the elements. +// +// Since: 1.4 +func NewHSplit(leading, trailing fyne.CanvasObject) *Split { + return newSplitContainer(true, leading, trailing) +} + +// NewVSplit creates a vertically arranged container with the specified top and bottom elements. +// A horizontal split bar that can be dragged will be added between the elements. +// +// Since: 1.4 +func NewVSplit(top, bottom fyne.CanvasObject) *Split { + return newSplitContainer(false, top, bottom) +} + +func newSplitContainer(horizontal bool, leading, trailing fyne.CanvasObject) *Split { + s := &Split{ + Offset: 0.5, // Sensible default, can be overridden with SetOffset + Horizontal: horizontal, + Leading: leading, + Trailing: trailing, + } + s.BaseWidget.ExtendBaseWidget(s) + return s +} + +// CreateRenderer is a private method to Fyne which links this widget to its renderer +func (s *Split) CreateRenderer() fyne.WidgetRenderer { + s.BaseWidget.ExtendBaseWidget(s) + d := newDivider(s) + return &splitContainerRenderer{ + split: s, + divider: d, + objects: []fyne.CanvasObject{s.Leading, d, s.Trailing}, + } +} + +// ExtendBaseWidget is used by an extending widget to make use of BaseWidget functionality. +// +// Deprecated: Support for extending containers is being removed +func (s *Split) ExtendBaseWidget(wid fyne.Widget) { + s.BaseWidget.ExtendBaseWidget(wid) +} + +// SetOffset sets the offset (0.0 to 1.0) of the Split divider. +// 0.0 - Leading is min size, Trailing uses all remaining space. +// 0.5 - Leading & Trailing equally share the available space. +// 1.0 - Trailing is min size, Leading uses all remaining space. +func (s *Split) SetOffset(offset float64) { + if s.Offset == offset { + return + } + s.Offset = offset + s.offsetUpdated = true + s.Refresh() +} + +var _ fyne.WidgetRenderer = (*splitContainerRenderer)(nil) + +type splitContainerRenderer struct { + split *Split + divider *divider + objects []fyne.CanvasObject +} + +func (r *splitContainerRenderer) Destroy() { +} + +func (r *splitContainerRenderer) Layout(size fyne.Size) { + var dividerPos, leadingPos, trailingPos fyne.Position + var dividerSize, leadingSize, trailingSize fyne.Size + + dividerVisible := r.split.Leading.Visible() && r.split.Trailing.Visible() + if !r.split.Leading.Visible() { + trailingPos = fyne.NewPos(0, 0) + trailingSize = size + } else if !r.split.Trailing.Visible() { + leadingPos = fyne.NewPos(0, 0) + leadingSize = size + } else if dividerVisible { + if r.split.Horizontal { + lw, tw := r.computeSplitLengths(size.Width, r.minLeadingWidth(), r.minTrailingWidth()) + leadingPos.X = 0 + leadingSize.Width = lw + leadingSize.Height = size.Height + dividerPos.X = lw + dividerSize.Width = dividerThickness(r.divider) + dividerSize.Height = size.Height + trailingPos.X = lw + dividerSize.Width + trailingSize.Width = tw + trailingSize.Height = size.Height + } else { + lh, th := r.computeSplitLengths(size.Height, r.minLeadingHeight(), r.minTrailingHeight()) + leadingPos.Y = 0 + leadingSize.Width = size.Width + leadingSize.Height = lh + dividerPos.Y = lh + dividerSize.Width = size.Width + dividerSize.Height = dividerThickness(r.divider) + trailingPos.Y = lh + dividerSize.Height + trailingSize.Width = size.Width + trailingSize.Height = th + } + } + + r.divider.Move(dividerPos) + r.divider.Resize(dividerSize) + r.divider.Hidden = !dividerVisible + + r.split.Leading.Move(leadingPos) + r.split.Leading.Resize(leadingSize) + r.split.Trailing.Move(trailingPos) + r.split.Trailing.Resize(trailingSize) + canvas.Refresh(r.divider) +} + +func (r *splitContainerRenderer) MinSize() fyne.Size { + s := fyne.NewSize(0, 0) + dividerVisible := r.split.Leading.Visible() && r.split.Trailing.Visible() + for i, o := range r.objects { + if (i == 1 /*divider*/ && !dividerVisible) || (i != 1 && !o.Visible()) { + continue + } + min := o.MinSize() + if r.split.Horizontal { + s.Width += min.Width + s.Height = fyne.Max(s.Height, min.Height) + } else { + s.Width = fyne.Max(s.Width, min.Width) + s.Height += min.Height + } + } + return s +} + +func (r *splitContainerRenderer) Objects() []fyne.CanvasObject { + return r.objects +} + +func (r *splitContainerRenderer) Refresh() { + if r.split.offsetUpdated { + r.Layout(r.split.Size()) + r.split.offsetUpdated = false + return + } + + r.objects[0] = r.split.Leading + // [1] is divider which doesn't change + r.objects[2] = r.split.Trailing + r.Layout(r.split.Size()) + + r.split.Leading.Refresh() + r.divider.Refresh() + r.split.Trailing.Refresh() + canvas.Refresh(r.split) +} + +func (r *splitContainerRenderer) computeSplitLengths(total, lMin, tMin float32) (float32, float32) { + available := float64(total - dividerThickness(r.divider)) + if available <= 0 { + return 0, 0 + } + ld := float64(lMin) + tr := float64(tMin) + offset := r.split.Offset + + min := ld / available + max := 1 - tr/available + if min <= max { + if offset < min { + offset = min + } + if offset > max { + offset = max + } + } else { + offset = ld / (ld + tr) + } + + ld = offset * available + tr = available - ld + return float32(ld), float32(tr) +} + +func (r *splitContainerRenderer) minLeadingWidth() float32 { + if r.split.Leading.Visible() { + return r.split.Leading.MinSize().Width + } + return 0 +} + +func (r *splitContainerRenderer) minLeadingHeight() float32 { + if r.split.Leading.Visible() { + return r.split.Leading.MinSize().Height + } + return 0 +} + +func (r *splitContainerRenderer) minTrailingWidth() float32 { + if r.split.Trailing.Visible() { + return r.split.Trailing.MinSize().Width + } + return 0 +} + +func (r *splitContainerRenderer) minTrailingHeight() float32 { + if r.split.Trailing.Visible() { + return r.split.Trailing.MinSize().Height + } + return 0 +} + +// Declare conformity with interfaces +var ( + _ fyne.CanvasObject = (*divider)(nil) + _ fyne.Draggable = (*divider)(nil) + _ desktop.Cursorable = (*divider)(nil) + _ desktop.Hoverable = (*divider)(nil) +) + +type divider struct { + widget.BaseWidget + split *Split + hovered bool + startDragOff *fyne.Position + currentDragPos fyne.Position +} + +func newDivider(split *Split) *divider { + d := ÷r{ + split: split, + } + d.ExtendBaseWidget(d) + return d +} + +// CreateRenderer is a private method to Fyne which links this widget to its renderer +func (d *divider) CreateRenderer() fyne.WidgetRenderer { + d.ExtendBaseWidget(d) + th := d.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + background := canvas.NewRectangle(th.Color(theme.ColorNameShadow, v)) + foreground := canvas.NewRectangle(th.Color(theme.ColorNameForeground, v)) + return ÷rRenderer{ + divider: d, + background: background, + foreground: foreground, + objects: []fyne.CanvasObject{background, foreground}, + } +} + +func (d *divider) Cursor() desktop.Cursor { + if d.split.Horizontal { + return desktop.HResizeCursor + } + return desktop.VResizeCursor +} + +func (d *divider) DragEnd() { + d.startDragOff = nil +} + +func (d *divider) Dragged(e *fyne.DragEvent) { + if d.startDragOff == nil { + d.currentDragPos = d.Position().Add(e.Position) + start := e.Position.Subtract(e.Dragged) + d.startDragOff = &start + } else { + d.currentDragPos = d.currentDragPos.Add(e.Dragged) + } + + x, y := d.currentDragPos.Components() + var offset, leadingRatio, trailingRatio float64 + if d.split.Horizontal { + widthFree := float64(d.split.Size().Width - dividerThickness(d)) + leadingRatio = float64(d.split.Leading.MinSize().Width) / widthFree + trailingRatio = 1. - (float64(d.split.Trailing.MinSize().Width) / widthFree) + offset = float64(x-d.startDragOff.X) / widthFree + } else { + heightFree := float64(d.split.Size().Height - dividerThickness(d)) + leadingRatio = float64(d.split.Leading.MinSize().Height) / heightFree + trailingRatio = 1. - (float64(d.split.Trailing.MinSize().Height) / heightFree) + offset = float64(y-d.startDragOff.Y) / heightFree + } + + if offset < leadingRatio { + offset = leadingRatio + } + if offset > trailingRatio { + offset = trailingRatio + } + d.split.SetOffset(offset) +} + +func (d *divider) MouseIn(event *desktop.MouseEvent) { + d.hovered = true + d.Refresh() +} + +func (d *divider) MouseMoved(event *desktop.MouseEvent) {} + +func (d *divider) MouseOut() { + d.hovered = false + d.Refresh() +} + +var _ fyne.WidgetRenderer = (*dividerRenderer)(nil) + +type dividerRenderer struct { + divider *divider + background *canvas.Rectangle + foreground *canvas.Rectangle + objects []fyne.CanvasObject +} + +func (r *dividerRenderer) Destroy() { +} + +func (r *dividerRenderer) Layout(size fyne.Size) { + r.background.Resize(size) + var x, y, w, h float32 + if r.divider.split.Horizontal { + x = (dividerThickness(r.divider) - handleThickness(r.divider)) / 2 + y = (size.Height - handleLength(r.divider)) / 2 + w = handleThickness(r.divider) + h = handleLength(r.divider) + } else { + x = (size.Width - handleLength(r.divider)) / 2 + y = (dividerThickness(r.divider) - handleThickness(r.divider)) / 2 + w = handleLength(r.divider) + h = handleThickness(r.divider) + } + r.foreground.Move(fyne.NewPos(x, y)) + r.foreground.Resize(fyne.NewSize(w, h)) +} + +func (r *dividerRenderer) MinSize() fyne.Size { + if r.divider.split.Horizontal { + return fyne.NewSize(dividerThickness(r.divider), dividerLength(r.divider)) + } + return fyne.NewSize(dividerLength(r.divider), dividerThickness(r.divider)) +} + +func (r *dividerRenderer) Objects() []fyne.CanvasObject { + return r.objects +} + +func (r *dividerRenderer) Refresh() { + th := r.divider.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + if r.divider.hovered { + r.background.FillColor = th.Color(theme.ColorNameHover, v) + } else { + r.background.FillColor = th.Color(theme.ColorNameShadow, v) + } + r.background.Refresh() + r.foreground.FillColor = th.Color(theme.ColorNameForeground, v) + r.foreground.Refresh() + r.Layout(r.divider.Size()) +} + +func dividerTheme(d *divider) fyne.Theme { + if d == nil { + return theme.Current() + } + + return d.Theme() +} + +func dividerThickness(d *divider) float32 { + th := dividerTheme(d) + return th.Size(theme.SizeNamePadding) * 2 +} + +func dividerLength(d *divider) float32 { + th := dividerTheme(d) + return th.Size(theme.SizeNamePadding) * 6 +} + +func handleThickness(d *divider) float32 { + th := dividerTheme(d) + return th.Size(theme.SizeNamePadding) / 2 +} + +func handleLength(d *divider) float32 { + th := dividerTheme(d) + return th.Size(theme.SizeNamePadding) * 4 +} diff --git a/vendor/fyne.io/fyne/v2/container/tabs.go b/vendor/fyne.io/fyne/v2/container/tabs.go new file mode 100644 index 0000000..32cd75e --- /dev/null +++ b/vendor/fyne.io/fyne/v2/container/tabs.go @@ -0,0 +1,881 @@ +package container + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/driver/desktop" + "fyne.io/fyne/v2/internal" + "fyne.io/fyne/v2/internal/build" + intTheme "fyne.io/fyne/v2/internal/theme" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" +) + +// TabItem represents a single view in a tab view. +// The Text and Icon are used for the tab button and the Content is shown when the corresponding tab is active. +// +// Since: 1.4 +type TabItem struct { + Text string + Icon fyne.Resource + Content fyne.CanvasObject + + button *tabButton + + disabled bool +} + +// Disabled returns whether or not the TabItem is disabled. +// +// Since: 2.3 +func (ti *TabItem) Disabled() bool { + return ti.disabled +} + +func (ti *TabItem) disable() { + ti.disabled = true + if ti.button != nil { + ti.button.Disable() + } +} + +func (ti *TabItem) enable() { + ti.disabled = false + if ti.button != nil { + ti.button.Enable() + } +} + +// TabLocation is the location where the tabs of a tab container should be rendered +// +// Since: 1.4 +type TabLocation int + +// TabLocation values +const ( + TabLocationTop TabLocation = iota + TabLocationLeading + TabLocationBottom + TabLocationTrailing +) + +// NewTabItem creates a new item for a tabbed widget - each item specifies the content and a label for its tab. +// +// Since: 1.4 +func NewTabItem(text string, content fyne.CanvasObject) *TabItem { + return &TabItem{Text: text, Content: content} +} + +// NewTabItemWithIcon creates a new item for a tabbed widget - each item specifies the content and a label with an icon for its tab. +// +// Since: 1.4 +func NewTabItemWithIcon(text string, icon fyne.Resource, content fyne.CanvasObject) *TabItem { + return &TabItem{Text: text, Icon: icon, Content: content} +} + +type baseTabs interface { + fyne.Widget + + onUnselected() func(*TabItem) + onSelected() func(*TabItem) + + items() []*TabItem + setItems([]*TabItem) + + selected() int + setSelected(int) + + tabLocation() TabLocation + + transitioning() bool + setTransitioning(bool) +} + +func isMobile(b baseTabs) bool { + d := fyne.CurrentDevice() + mobile := intTheme.FeatureForWidget(intTheme.FeatureNameDeviceIsMobile, b) + if is, ok := mobile.(bool); ok { + return is + } + + return d.IsMobile() +} + +func tabsAdjustedLocation(l TabLocation, b baseTabs) TabLocation { + // Mobile has limited screen space, so don't put app tab bar on long edges + if isMobile(b) { + if o := fyne.CurrentDevice().Orientation(); fyne.IsVertical(o) { + if l == TabLocationLeading { + return TabLocationTop + } else if l == TabLocationTrailing { + return TabLocationBottom + } + } else { + if l == TabLocationTop { + return TabLocationLeading + } else if l == TabLocationBottom { + return TabLocationTrailing + } + } + } + + return l +} + +func buildPopUpMenu(t baseTabs, button *widget.Button, items []*fyne.MenuItem) *widget.PopUpMenu { + d := fyne.CurrentApp().Driver() + c := d.CanvasForObject(button) + popUpMenu := widget.NewPopUpMenu(fyne.NewMenu("", items...), c) + buttonPos := d.AbsolutePositionForObject(button) + buttonSize := button.Size() + popUpMin := popUpMenu.MinSize() + var popUpPos fyne.Position + switch t.tabLocation() { + case TabLocationLeading: + popUpPos.X = buttonPos.X + buttonSize.Width + popUpPos.Y = buttonPos.Y + buttonSize.Height - popUpMin.Height + case TabLocationTrailing: + popUpPos.X = buttonPos.X - popUpMin.Width + popUpPos.Y = buttonPos.Y + buttonSize.Height - popUpMin.Height + case TabLocationTop: + popUpPos.X = buttonPos.X + buttonSize.Width - popUpMin.Width + popUpPos.Y = buttonPos.Y + buttonSize.Height + case TabLocationBottom: + popUpPos.X = buttonPos.X + buttonSize.Width - popUpMin.Width + popUpPos.Y = buttonPos.Y - popUpMin.Height + } + if popUpPos.X < 0 { + popUpPos.X = 0 + } + if popUpPos.Y < 0 { + popUpPos.Y = 0 + } + popUpMenu.ShowAtPosition(popUpPos) + return popUpMenu +} + +func removeIndex(t baseTabs, index int) { + items := t.items() + if index < 0 || index >= len(items) { + return + } + setItems(t, append(items[:index], items[index+1:]...)) + if s := t.selected(); index < s { + t.setSelected(s - 1) + } +} + +func removeItem(t baseTabs, item *TabItem) { + for index, existingItem := range t.items() { + if existingItem == item { + removeIndex(t, index) + break + } + } +} + +func selected(t baseTabs) *TabItem { + selected := t.selected() + items := t.items() + if selected < 0 || selected >= len(items) { + return nil + } + return items[selected] +} + +func selectIndex(t baseTabs, index int) { + selected := t.selected() + + if selected == index { + // No change, so do nothing + return + } + + items := t.items() + + if f := t.onUnselected(); f != nil && selected >= 0 && selected < len(items) { + // Notification of unselected + f(items[selected]) + } + + if index < 0 || index >= len(items) { + // Out of bounds, so do nothing + return + } + + t.setTransitioning(true) + t.setSelected(index) + t.Refresh() + + if f := t.onSelected(); f != nil { + // Notification of selected + f(items[index]) + } +} + +func selectItem(t baseTabs, item *TabItem) { + for i, child := range t.items() { + if child == item { + selectIndex(t, i) + return + } + } +} + +func setItems(t baseTabs, items []*TabItem) { + if build.HasHints && mismatchedTabItems(items) { + internal.LogHint("Tab items should all have the same type of content (text, icons or both)") + } + t.setItems(items) + selected := t.selected() + count := len(items) + switch { + case count == 0: + // No items available to be selected + selectIndex(t, -1) // Unsure OnUnselected gets called if applicable + t.setSelected(-1) + case selected < 0: + // Current is first tab item + selectIndex(t, 0) + case selected >= count: + // Current doesn't exist, select last tab + selectIndex(t, count-1) + } +} + +func disableIndex(t baseTabs, index int) { + items := t.items() + if index < 0 || index >= len(items) { + return + } + + item := items[index] + item.disable() + + if selected(t) == item { + // the disabled tab is currently selected, so select the first enabled tab + for i, it := range items { + if !it.Disabled() { + selectIndex(t, i) + break + } + } + } + + if selected(t) == item { + selectIndex(t, -1) // no other tab is able to be selected + } +} + +func disableItem(t baseTabs, item *TabItem) { + for i, it := range t.items() { + if it == item { + disableIndex(t, i) + return + } + } +} + +func enableIndex(t baseTabs, index int) { + items := t.items() + if index < 0 || index >= len(items) { + return + } + + item := items[index] + item.enable() +} + +func enableItem(t baseTabs, item *TabItem) { + for i, it := range t.items() { + if it == item { + enableIndex(t, i) + return + } + } +} + +type baseTabsRenderer struct { + positionAnimation, sizeAnimation *fyne.Animation + + lastIndicatorPos fyne.Position + lastIndicatorSize fyne.Size + lastIndicatorHidden bool + + action *widget.Button + bar *fyne.Container + divider, indicator *canvas.Rectangle + + tabs baseTabs +} + +func (r *baseTabsRenderer) Destroy() { +} + +func (r *baseTabsRenderer) applyTheme(t baseTabs) { + if r.action != nil { + r.action.SetIcon(moreIcon(t)) + } + th := theme.CurrentForWidget(t) + v := fyne.CurrentApp().Settings().ThemeVariant() + + r.divider.FillColor = th.Color(theme.ColorNameShadow, v) + r.indicator.FillColor = th.Color(theme.ColorNamePrimary, v) + r.indicator.CornerRadius = th.Size(theme.SizeNameSelectionRadius) + + for _, tab := range r.tabs.items() { + tab.Content.Refresh() + } +} + +func (r *baseTabsRenderer) layout(t baseTabs, size fyne.Size) { + var ( + barPos, dividerPos, contentPos fyne.Position + barSize, dividerSize, contentSize fyne.Size + ) + + barMin := r.bar.MinSize() + + th := theme.CurrentForWidget(t) + padding := th.Size(theme.SizeNamePadding) + switch t.tabLocation() { + case TabLocationTop: + barHeight := barMin.Height + barPos = fyne.NewPos(0, 0) + barSize = fyne.NewSize(size.Width, barHeight) + dividerPos = fyne.NewPos(0, barHeight) + dividerSize = fyne.NewSize(size.Width, padding) + contentPos = fyne.NewPos(0, barHeight+padding) + contentSize = fyne.NewSize(size.Width, size.Height-barHeight-padding) + case TabLocationLeading: + barWidth := barMin.Width + barPos = fyne.NewPos(0, 0) + barSize = fyne.NewSize(barWidth, size.Height) + dividerPos = fyne.NewPos(barWidth, 0) + dividerSize = fyne.NewSize(padding, size.Height) + contentPos = fyne.NewPos(barWidth+padding, 0) + contentSize = fyne.NewSize(size.Width-barWidth-padding, size.Height) + case TabLocationBottom: + barHeight := barMin.Height + barPos = fyne.NewPos(0, size.Height-barHeight) + barSize = fyne.NewSize(size.Width, barHeight) + dividerPos = fyne.NewPos(0, size.Height-barHeight-padding) + dividerSize = fyne.NewSize(size.Width, padding) + contentPos = fyne.NewPos(0, 0) + contentSize = fyne.NewSize(size.Width, size.Height-barHeight-padding) + case TabLocationTrailing: + barWidth := barMin.Width + barPos = fyne.NewPos(size.Width-barWidth, 0) + barSize = fyne.NewSize(barWidth, size.Height) + dividerPos = fyne.NewPos(size.Width-barWidth-padding, 0) + dividerSize = fyne.NewSize(padding, size.Height) + contentPos = fyne.NewPos(0, 0) + contentSize = fyne.NewSize(size.Width-barWidth-padding, size.Height) + } + + r.bar.Move(barPos) + r.bar.Resize(barSize) + r.divider.Move(dividerPos) + r.divider.Resize(dividerSize) + selected := t.selected() + for i, ti := range t.items() { + if i == selected { + ti.Content.Move(contentPos) + ti.Content.Resize(contentSize) + ti.Content.Show() + } else { + ti.Content.Hide() + } + } +} + +func (r *baseTabsRenderer) minSize(t baseTabs) fyne.Size { + th := theme.CurrentForWidget(t) + pad := th.Size(theme.SizeNamePadding) + buttonPad := pad + barMin := r.bar.MinSize() + tabsMin := r.bar.Objects[0].MinSize() + accessory := r.bar.Objects[1] + accessoryMin := accessory.MinSize() + if scroll, ok := r.bar.Objects[0].(*Scroll); ok && len(scroll.Content.(*fyne.Container).Objects) == 0 { + tabsMin = fyne.Size{} // scroller forces 32 where we don't need any space + buttonPad = 0 + } else if group, ok := r.bar.Objects[0].(*fyne.Container); ok && len(group.Objects) > 0 { + tabsMin = group.Objects[0].MinSize() + buttonPad = 0 + } + if !accessory.Visible() || accessoryMin.Width == 0 { + buttonPad = 0 + accessoryMin = fyne.Size{} + } + + contentMin := fyne.NewSize(0, 0) + for _, content := range t.items() { + contentMin = contentMin.Max(content.Content.MinSize()) + } + + switch t.tabLocation() { + case TabLocationLeading, TabLocationTrailing: + return fyne.NewSize(barMin.Width+contentMin.Width+pad, + fyne.Max(contentMin.Height, accessoryMin.Height+buttonPad+tabsMin.Height)) + default: + return fyne.NewSize(fyne.Max(contentMin.Width, accessoryMin.Width+buttonPad+tabsMin.Width), + barMin.Height+contentMin.Height+pad) + } +} + +func (r *baseTabsRenderer) moveIndicator(pos fyne.Position, siz fyne.Size, th fyne.Theme, animate bool) { + isSameState := r.lastIndicatorPos == pos && r.lastIndicatorSize == siz && + r.lastIndicatorHidden == r.indicator.Hidden + if isSameState { + return + } + + if r.positionAnimation != nil { + r.positionAnimation.Stop() + r.positionAnimation = nil + } + if r.sizeAnimation != nil { + r.sizeAnimation.Stop() + r.sizeAnimation = nil + } + + v := fyne.CurrentApp().Settings().ThemeVariant() + r.indicator.FillColor = th.Color(theme.ColorNamePrimary, v) + if r.indicator.Position().IsZero() { + r.indicator.Move(pos) + r.indicator.Resize(siz) + r.indicator.Refresh() + return + } + + r.lastIndicatorPos = pos + r.lastIndicatorSize = siz + r.lastIndicatorHidden = r.indicator.Hidden + + if animate && fyne.CurrentApp().Settings().ShowAnimations() { + r.positionAnimation = canvas.NewPositionAnimation(r.indicator.Position(), pos, canvas.DurationShort, func(p fyne.Position) { + r.indicator.Move(p) + r.indicator.Refresh() + if pos == p { + r.positionAnimation.Stop() + r.positionAnimation = nil + } + }) + r.sizeAnimation = canvas.NewSizeAnimation(r.indicator.Size(), siz, canvas.DurationShort, func(s fyne.Size) { + r.indicator.Resize(s) + r.indicator.Refresh() + if siz == s { + r.sizeAnimation.Stop() + r.sizeAnimation = nil + } + }) + + r.positionAnimation.Start() + r.sizeAnimation.Start() + } else { + r.indicator.Move(pos) + r.indicator.Resize(siz) + r.indicator.Refresh() + } +} + +func (r *baseTabsRenderer) objects(t baseTabs) []fyne.CanvasObject { + objects := []fyne.CanvasObject{r.bar, r.divider, r.indicator} + if i, is := t.selected(), t.items(); i >= 0 && i < len(is) { + objects = append(objects, is[i].Content) + } + return objects +} + +func (r *baseTabsRenderer) refresh(t baseTabs) { + r.applyTheme(t) + + r.bar.Refresh() + r.divider.Refresh() + r.indicator.Refresh() +} + +type buttonIconPosition int + +const ( + buttonIconInline buttonIconPosition = iota + buttonIconTop +) + +var ( + _ fyne.Widget = (*tabButton)(nil) + _ fyne.Tappable = (*tabButton)(nil) + _ desktop.Hoverable = (*tabButton)(nil) +) + +type tabButton struct { + widget.DisableableWidget + hovered bool + icon fyne.Resource + iconPosition buttonIconPosition + importance widget.Importance + onTapped func() + onClosed func() + text string + textAlignment fyne.TextAlign + + tabs baseTabs +} + +func (b *tabButton) CreateRenderer() fyne.WidgetRenderer { + b.ExtendBaseWidget(b) + th := b.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + background := canvas.NewRectangle(th.Color(theme.ColorNameHover, v)) + background.CornerRadius = th.Size(theme.SizeNameSelectionRadius) + background.Hide() + icon := canvas.NewImageFromResource(b.icon) + if b.icon == nil { + icon.Hide() + } + + label := canvas.NewText(b.text, th.Color(theme.ColorNameForeground, v)) + label.TextStyle.Bold = true + + close := &tabCloseButton{ + parent: b, + onTapped: func() { + if f := b.onClosed; f != nil { + f() + } + }, + } + close.ExtendBaseWidget(close) + close.Hide() + + objects := []fyne.CanvasObject{background, label, close, icon} + return &tabButtonRenderer{ + button: b, + background: background, + icon: icon, + label: label, + close: close, + objects: objects, + } +} + +func (b *tabButton) MinSize() fyne.Size { + b.ExtendBaseWidget(b) + return b.BaseWidget.MinSize() +} + +func (b *tabButton) MouseIn(*desktop.MouseEvent) { + b.hovered = true + b.Refresh() +} + +func (b *tabButton) MouseMoved(*desktop.MouseEvent) { +} + +func (b *tabButton) MouseOut() { + b.hovered = false + b.Refresh() +} + +func (b *tabButton) Tapped(*fyne.PointEvent) { + if b.Disabled() { + return + } + + b.onTapped() +} + +type tabButtonRenderer struct { + button *tabButton + background *canvas.Rectangle + icon *canvas.Image + label *canvas.Text + close *tabCloseButton + objects []fyne.CanvasObject +} + +func (r *tabButtonRenderer) Destroy() { +} + +func (r *tabButtonRenderer) Layout(size fyne.Size) { + th := r.button.Theme() + pad := th.Size(theme.SizeNamePadding) + r.background.Resize(size) + padding := r.padding() + innerSize := size.Subtract(padding) + innerOffset := fyne.NewPos(padding.Width/2, padding.Height/2) + labelShift := float32(0) + if r.icon.Visible() { + iconSize := r.iconSize() + var iconOffset fyne.Position + if r.button.iconPosition == buttonIconTop { + iconOffset = fyne.NewPos((innerSize.Width-iconSize)/2, 0) + } else { + iconOffset = fyne.NewPos(0, (innerSize.Height-iconSize)/2) + } + r.icon.Resize(fyne.NewSquareSize(iconSize)) + r.icon.Move(innerOffset.Add(iconOffset)) + labelShift = iconSize + pad + } + if r.label.Text != "" { + var labelOffset fyne.Position + var labelSize fyne.Size + if r.button.iconPosition == buttonIconTop { + labelOffset = fyne.NewPos(0, labelShift) + labelSize = fyne.NewSize(innerSize.Width, r.label.MinSize().Height) + } else { + labelOffset = fyne.NewPos(labelShift, 0) + labelSize = fyne.NewSize(innerSize.Width-labelShift, innerSize.Height) + } + r.label.Resize(labelSize) + r.label.Move(innerOffset.Add(labelOffset)) + } + inlineIconSize := th.Size(theme.SizeNameInlineIcon) + r.close.Move(fyne.NewPos(size.Width-inlineIconSize-pad, (size.Height-inlineIconSize)/2)) + r.close.Resize(fyne.NewSquareSize(inlineIconSize)) +} + +func (r *tabButtonRenderer) MinSize() fyne.Size { + th := r.button.Theme() + var contentWidth, contentHeight float32 + textSize := r.label.MinSize() + iconSize := r.iconSize() + padding := th.Size(theme.SizeNamePadding) + if r.button.iconPosition == buttonIconTop { + contentWidth = fyne.Max(textSize.Width, iconSize) + if r.icon.Visible() { + contentHeight += iconSize + } + if r.label.Text != "" { + if r.icon.Visible() { + contentHeight += padding + } + contentHeight += textSize.Height + } + } else { + contentHeight = fyne.Max(textSize.Height, iconSize) + if r.icon.Visible() { + contentWidth += iconSize + } + if r.label.Text != "" { + if r.icon.Visible() { + contentWidth += padding + } + contentWidth += textSize.Width + } + } + if r.button.onClosed != nil { + inlineIconSize := th.Size(theme.SizeNameInlineIcon) + contentWidth += inlineIconSize + padding + contentHeight = fyne.Max(contentHeight, inlineIconSize) + } + return fyne.NewSize(contentWidth, contentHeight).Add(r.padding()) +} + +func (r *tabButtonRenderer) Objects() []fyne.CanvasObject { + return r.objects +} + +func (r *tabButtonRenderer) Refresh() { + th := r.button.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + if r.button.hovered && !r.button.Disabled() { + r.background.FillColor = th.Color(theme.ColorNameHover, v) + r.background.CornerRadius = th.Size(theme.SizeNameSelectionRadius) + r.background.Show() + } else { + r.background.Hide() + } + r.background.Refresh() + + r.label.Text = r.button.text + r.label.Alignment = r.button.textAlignment + if !r.button.Disabled() { + if r.button.importance == widget.HighImportance { + r.label.Color = th.Color(theme.ColorNamePrimary, v) + } else { + r.label.Color = th.Color(theme.ColorNameForeground, v) + } + } else { + r.label.Color = th.Color(theme.ColorNameDisabled, v) + } + r.label.TextSize = th.Size(theme.SizeNameText) + if r.button.text == "" { + r.label.Hide() + } else { + r.label.Show() + } + + r.icon.Resource = r.button.icon + if r.icon.Resource != nil { + r.icon.Show() + switch res := r.icon.Resource.(type) { + case *theme.ThemedResource: + if r.button.importance == widget.HighImportance { + r.icon.Resource = theme.NewPrimaryThemedResource(res) + } + case *theme.PrimaryThemedResource: + if r.button.importance != widget.HighImportance { + r.icon.Resource = res.Original() + } + } + r.icon.Refresh() + } else { + r.icon.Hide() + } + + if r.button.onClosed != nil && (isMobile(r.button.tabs) || r.button.hovered || r.close.hovered) { + r.close.Show() + } else { + r.close.Hide() + } + r.close.Refresh() + + canvas.Refresh(r.button) +} + +func (r *tabButtonRenderer) iconSize() float32 { + iconSize := r.button.Theme().Size(theme.SizeNameInlineIcon) + if r.button.iconPosition == buttonIconTop { + return 2 * iconSize + } + + return iconSize +} + +func (r *tabButtonRenderer) padding() fyne.Size { + padding := r.button.Theme().Size(theme.SizeNameInnerPadding) + if r.label.Text != "" && r.button.iconPosition == buttonIconInline { + return fyne.NewSquareSize(padding * 2) + } + return fyne.NewSize(padding, padding*2) +} + +var ( + _ fyne.Widget = (*tabCloseButton)(nil) + _ fyne.Tappable = (*tabCloseButton)(nil) + _ desktop.Hoverable = (*tabCloseButton)(nil) +) + +type tabCloseButton struct { + widget.BaseWidget + parent *tabButton + hovered bool + onTapped func() +} + +func (b *tabCloseButton) CreateRenderer() fyne.WidgetRenderer { + b.ExtendBaseWidget(b) + th := b.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + background := canvas.NewRectangle(th.Color(theme.ColorNameHover, v)) + background.CornerRadius = th.Size(theme.SizeNameSelectionRadius) + background.Hide() + icon := canvas.NewImageFromResource(theme.CancelIcon()) + + return &tabCloseButtonRenderer{ + button: b, + background: background, + icon: icon, + objects: []fyne.CanvasObject{background, icon}, + } +} + +func (b *tabCloseButton) MinSize() fyne.Size { + b.ExtendBaseWidget(b) + return b.BaseWidget.MinSize() +} + +func (b *tabCloseButton) MouseIn(*desktop.MouseEvent) { + b.hovered = true + b.parent.Refresh() +} + +func (b *tabCloseButton) MouseMoved(*desktop.MouseEvent) { +} + +func (b *tabCloseButton) MouseOut() { + b.hovered = false + b.parent.Refresh() +} + +func (b *tabCloseButton) Tapped(*fyne.PointEvent) { + b.onTapped() +} + +type tabCloseButtonRenderer struct { + button *tabCloseButton + background *canvas.Rectangle + icon *canvas.Image + objects []fyne.CanvasObject +} + +func (r *tabCloseButtonRenderer) Destroy() { +} + +func (r *tabCloseButtonRenderer) Layout(size fyne.Size) { + r.background.Resize(size) + r.icon.Resize(size) +} + +func (r *tabCloseButtonRenderer) MinSize() fyne.Size { + return fyne.NewSquareSize(r.button.Theme().Size(theme.SizeNameInlineIcon)) +} + +func (r *tabCloseButtonRenderer) Objects() []fyne.CanvasObject { + return r.objects +} + +func (r *tabCloseButtonRenderer) Refresh() { + th := r.button.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + if r.button.hovered { + r.background.FillColor = th.Color(theme.ColorNameHover, v) + r.background.CornerRadius = th.Size(theme.SizeNameSelectionRadius) + r.background.Show() + } else { + r.background.Hide() + } + r.background.Refresh() + switch res := r.icon.Resource.(type) { + case *theme.ThemedResource: + if r.button.parent.importance == widget.HighImportance { + r.icon.Resource = theme.NewPrimaryThemedResource(res) + } + case *theme.PrimaryThemedResource: + if r.button.parent.importance != widget.HighImportance { + r.icon.Resource = res.Original() + } + } + r.icon.Refresh() +} + +func mismatchedTabItems(items []*TabItem) bool { + var hasText, hasIcon bool + for _, tab := range items { + hasText = hasText || tab.Text != "" + hasIcon = hasIcon || tab.Icon != nil + } + + mismatch := false + for _, tab := range items { + if (hasText && tab.Text == "") || (hasIcon && tab.Icon == nil) { + mismatch = true + break + } + } + + return mismatch +} + +func moreIcon(t baseTabs) fyne.Resource { + if l := t.tabLocation(); l == TabLocationLeading || l == TabLocationTrailing { + return theme.MoreVerticalIcon() + } + return theme.MoreHorizontalIcon() +} diff --git a/vendor/fyne.io/fyne/v2/container/theme.go b/vendor/fyne.io/fyne/v2/container/theme.go new file mode 100644 index 0000000..ed3b53b --- /dev/null +++ b/vendor/fyne.io/fyne/v2/container/theme.go @@ -0,0 +1,116 @@ +package container + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/cache" + intTheme "fyne.io/fyne/v2/internal/theme" + "fyne.io/fyne/v2/widget" +) + +// ThemeOverride is a container where the child widgets are themed by the specified theme. +// Containers will be traversed and all child widgets will reflect the theme in this container. +// This should be used sparingly to avoid a jarring user experience. +// +// Since: 2.5 +type ThemeOverride struct { + widget.BaseWidget + + Content fyne.CanvasObject + Theme fyne.Theme + + holder *fyne.Container + + mobile bool +} + +// NewThemeOverride provides a container where the child widgets are themed by the specified theme. +// Containers will be traversed and all child widgets will reflect the theme in this container. +// This should be used sparingly to avoid a jarring user experience. +// +// If the content `obj` of this theme override is a container and items are later added to the container or any +// sub-containers ensure that you call `Refresh()` on this `ThemeOverride` to ensure the new items match the theme. +// +// Since: 2.5 +func NewThemeOverride(obj fyne.CanvasObject, th fyne.Theme) *ThemeOverride { + t := &ThemeOverride{Content: obj, Theme: th, holder: NewStack(obj)} + t.ExtendBaseWidget(t) + + cache.OverrideTheme(obj, addFeatures(th, t)) + obj.Refresh() // required as the widgets passed in could have been initially rendered with default theme + return t +} + +func (t *ThemeOverride) CreateRenderer() fyne.WidgetRenderer { + cache.OverrideTheme(t.Content, addFeatures(t.Theme, t)) + + return &overrideRenderer{parent: t, objs: []fyne.CanvasObject{t.holder}} +} + +func (t *ThemeOverride) Refresh() { + if t.holder.Objects[0] != t.Content { + t.holder.Objects[0] = t.Content + t.holder.Refresh() + } + + cache.OverrideTheme(t.Content, addFeatures(t.Theme, t)) + t.Content.Refresh() + t.BaseWidget.Refresh() +} + +// SetDeviceIsMobile allows a ThemeOverride container to shape the contained widgets as a mobile device. +// This will impact containers such as AppTabs and DocTabs, and more in the future, to display a layout +// that would automatically be used for a mobile device runtime. +// +// Since: 2.6 +func (t *ThemeOverride) SetDeviceIsMobile(on bool) { + t.mobile = on + t.BaseWidget.Refresh() +} + +type featureTheme struct { + fyne.Theme + + over *ThemeOverride +} + +func addFeatures(th fyne.Theme, o *ThemeOverride) fyne.Theme { + return &featureTheme{Theme: th, over: o} +} + +func (f *featureTheme) Feature(n intTheme.FeatureName) any { + if n == intTheme.FeatureNameDeviceIsMobile { + return f.over.mobile + } + + return nil +} + +type overrideRenderer struct { + parent *ThemeOverride + + objs []fyne.CanvasObject +} + +func (r *overrideRenderer) Destroy() { +} + +func (r *overrideRenderer) Layout(s fyne.Size) { + intTheme.PushRenderingTheme(r.parent.Theme) + defer intTheme.PopRenderingTheme() + + r.parent.holder.Resize(s) +} + +func (r *overrideRenderer) MinSize() fyne.Size { + intTheme.PushRenderingTheme(r.parent.Theme) + defer intTheme.PopRenderingTheme() + + return r.parent.Content.MinSize() +} + +func (r *overrideRenderer) Objects() []fyne.CanvasObject { + return r.objs +} + +func (r *overrideRenderer) Refresh() { +} diff --git a/vendor/fyne.io/fyne/v2/data/binding/binding.go b/vendor/fyne.io/fyne/v2/data/binding/binding.go new file mode 100644 index 0000000..6e9077c --- /dev/null +++ b/vendor/fyne.io/fyne/v2/data/binding/binding.go @@ -0,0 +1,169 @@ +//go:generate go run gen.go + +// Package binding provides support for binding data to widgets. +// All APIs in the binding package are safe to invoke directly from any goroutine. +package binding + +import ( + "errors" + "reflect" + "sync" + + "fyne.io/fyne/v2" +) + +var ( + errKeyNotFound = errors.New("key not found") + errOutOfBounds = errors.New("index out of bounds") + errParseFailed = errors.New("format did not match 1 value") + + // As an optimisation we connect any listeners asking for the same key, so that there is only 1 per preference item. + prefBinds = newPreferencesMap() +) + +// DataItem is the base interface for all bindable data items. +// All APIs on bindable data items are safe to invoke directly fron any goroutine. +// +// Since: 2.0 +type DataItem interface { + // AddListener attaches a new change listener to this DataItem. + // Listeners are called each time the data inside this DataItem changes. + // Additionally, the listener will be triggered upon successful connection to get the current value. + AddListener(DataListener) + // RemoveListener will detach the specified change listener from the DataItem. + // Disconnected listener will no longer be triggered when changes occur. + RemoveListener(DataListener) +} + +// DataListener is any object that can register for changes in a bindable DataItem. +// See NewDataListener to define a new listener using just an inline function. +// +// Since: 2.0 +type DataListener interface { + DataChanged() +} + +// NewDataListener is a helper function that creates a new listener type from a simple callback function. +// +// Since: 2.0 +func NewDataListener(fn func()) DataListener { + return &listener{fn} +} + +type listener struct { + callback func() +} + +func (l *listener) DataChanged() { + l.callback() +} + +type base struct { + listeners []DataListener + + lock sync.RWMutex +} + +// AddListener allows a data listener to be informed of changes to this item. +func (b *base) AddListener(l DataListener) { + fyne.Do(func() { + b.listeners = append(b.listeners, l) + l.DataChanged() + }) +} + +// RemoveListener should be called if the listener is no longer interested in being informed of data change events. +func (b *base) RemoveListener(l DataListener) { + fyne.Do(func() { + for i, listener := range b.listeners { + if listener == l { + // Delete without preserving order: + lastIndex := len(b.listeners) - 1 + b.listeners[i] = b.listeners[lastIndex] + b.listeners[lastIndex] = nil + b.listeners = b.listeners[:lastIndex] + return + } + } + }) +} + +func (b *base) trigger() { + fyne.Do(b.triggerFromMain) +} + +func (b *base) triggerFromMain() { + for _, listen := range b.listeners { + listen.DataChanged() + } +} + +// Untyped supports binding an any value. +// +// Since: 2.1 +type Untyped = Item[any] + +// NewUntyped returns a bindable any value that is managed internally. +// +// Since: 2.1 +func NewUntyped() Untyped { + return NewItem(func(a1, a2 any) bool { return a1 == a2 }) +} + +// ExternalUntyped supports binding a any value to an external value. +// +// Since: 2.1 +type ExternalUntyped = ExternalItem[any] + +// BindUntyped returns a bindable any value that is bound to an external type. +// The parameter must be a pointer to the type you wish to bind. +// +// Since: 2.1 +func BindUntyped(v any) ExternalUntyped { + t := reflect.TypeOf(v) + if t.Kind() != reflect.Ptr { + fyne.LogError("Invalid type passed to BindUntyped, must be a pointer", nil) + v = nil + } + + if v == nil { + v = new(any) // never allow a nil value pointer + } + + b := &boundExternalUntyped{} + b.val = reflect.ValueOf(v).Elem() + b.old = b.val.Interface() + return b +} + +type boundExternalUntyped struct { + base + + val reflect.Value + old any +} + +func (b *boundExternalUntyped) Get() (any, error) { + b.lock.RLock() + defer b.lock.RUnlock() + + return b.val.Interface(), nil +} + +func (b *boundExternalUntyped) Set(val any) error { + b.lock.Lock() + if b.old == val { + b.lock.Unlock() + return nil + } + b.val.Set(reflect.ValueOf(val)) + b.old = val + b.lock.Unlock() + + b.trigger() + return nil +} + +func (b *boundExternalUntyped) Reload() error { + return b.Set(b.val.Interface()) +} diff --git a/vendor/fyne.io/fyne/v2/data/binding/bool.go b/vendor/fyne.io/fyne/v2/data/binding/bool.go new file mode 100644 index 0000000..63632b9 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/data/binding/bool.go @@ -0,0 +1,118 @@ +package binding + +type not struct { + Bool +} + +var _ Bool = (*not)(nil) + +// Not returns a Bool binding that invert the value of the given data binding. +// This is providing the logical Not boolean operation as a data binding. +// +// Since 2.4 +func Not(data Bool) Bool { + return ¬{Bool: data} +} + +func (n *not) Get() (bool, error) { + v, err := n.Bool.Get() + return !v, err +} + +func (n *not) Set(value bool) error { + return n.Bool.Set(!value) +} + +type and struct { + booleans +} + +var _ Bool = (*and)(nil) + +// And returns a Bool binding that return true when all the passed Bool binding are +// true and false otherwise. It does apply a logical and boolean operation on all passed +// Bool bindings. This binding is two way. In case of a Set, it will propagate the value +// identically to all the Bool bindings used for its construction. +// +// Since 2.4 +func And(data ...Bool) Bool { + return &and{booleans: booleans{data: data}} +} + +func (a *and) Get() (bool, error) { + for _, d := range a.data { + v, err := d.Get() + if err != nil { + return false, err + } + if !v { + return false, nil + } + } + return true, nil +} + +func (a *and) Set(value bool) error { + for _, d := range a.data { + err := d.Set(value) + if err != nil { + return err + } + } + return nil +} + +type or struct { + booleans +} + +var _ Bool = (*or)(nil) + +// Or returns a Bool binding that return true when at least one of the passed Bool binding +// is true and false otherwise. It does apply a logical or boolean operation on all passed +// Bool bindings. This binding is two way. In case of a Set, it will propagate the value +// identically to all the Bool bindings used for its construction. +// +// Since 2.4 +func Or(data ...Bool) Bool { + return &or{booleans: booleans{data: data}} +} + +func (o *or) Get() (bool, error) { + for _, d := range o.data { + v, err := d.Get() + if err != nil { + return false, err + } + if v { + return true, nil + } + } + return false, nil +} + +func (o *or) Set(value bool) error { + for _, d := range o.data { + err := d.Set(value) + if err != nil { + return err + } + } + return nil +} + +type booleans struct { + data []Bool +} + +func (g *booleans) AddListener(listener DataListener) { + for _, d := range g.data { + d.AddListener(listener) + } +} + +func (g *booleans) RemoveListener(listener DataListener) { + for _, d := range g.data { + d.RemoveListener(listener) + } +} diff --git a/vendor/fyne.io/fyne/v2/data/binding/convert.go b/vendor/fyne.io/fyne/v2/data/binding/convert.go new file mode 100644 index 0000000..4d48c9c --- /dev/null +++ b/vendor/fyne.io/fyne/v2/data/binding/convert.go @@ -0,0 +1,409 @@ +package binding + +import ( + "fmt" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/storage" +) + +// BoolToString creates a binding that connects a Bool data item to a String. +// Changes to the Bool will be pushed to the String and setting the string will parse and set the +// Bool if the parse was successful. +// +// Since: 2.0 +func BoolToString(v Bool) String { + return toStringComparable(v, formatBool, parseBool) +} + +// BoolToStringWithFormat creates a binding that connects a Bool data item to a String and is +// presented using the specified format. Changes to the Bool will be pushed to the String and setting +// the string will parse and set the Bool if the string matches the format and its parse was successful. +// +// Since: 2.0 +func BoolToStringWithFormat(v Bool, format string) String { + return toStringWithFormatComparable[bool](v, format, "%t", formatBool, parseBool) +} + +// FloatToString creates a binding that connects a Float data item to a String. +// Changes to the Float will be pushed to the String and setting the string will parse and set the +// Float if the parse was successful. +// +// Since: 2.0 +func FloatToString(v Float) String { + return toStringComparable(v, formatFloat, parseFloat) +} + +// FloatToStringWithFormat creates a binding that connects a Float data item to a String and is +// presented using the specified format. Changes to the Float will be pushed to the String and setting +// the string will parse and set the Float if the string matches the format and its parse was successful. +// +// Since: 2.0 +func FloatToStringWithFormat(v Float, format string) String { + return toStringWithFormatComparable(v, format, "%f", formatFloat, parseFloat) +} + +// IntToFloat creates a binding that connects an Int data item to a Float. +// +// Since: 2.5 +func IntToFloat(val Int) Float { + v := &fromIntTo[float64]{from: val, parser: internalFloatToInt, formatter: internalIntToFloat} + val.AddListener(v) + return v +} + +// FloatToInt creates a binding that connects a Float data item to an Int. +// +// Since: 2.5 +func FloatToInt(v Float) Int { + i := &toInt[float64]{from: v, parser: internalFloatToInt, formatter: internalIntToFloat} + v.AddListener(i) + return i +} + +// IntToString creates a binding that connects a Int data item to a String. +// Changes to the Int will be pushed to the String and setting the string will parse and set the +// Int if the parse was successful. +// +// Since: 2.0 +func IntToString(v Int) String { + return toStringComparable(v, formatInt, parseInt) +} + +// IntToStringWithFormat creates a binding that connects a Int data item to a String and is +// presented using the specified format. Changes to the Int will be pushed to the String and setting +// the string will parse and set the Int if the string matches the format and its parse was successful. +// +// Since: 2.0 +func IntToStringWithFormat(v Int, format string) String { + return toStringWithFormatComparable(v, format, "%d", formatInt, parseInt) +} + +// URIToString creates a binding that connects a URI data item to a String. +// Changes to the URI will be pushed to the String and setting the string will parse and set the +// URI if the parse was successful. +// +// Since: 2.1 +func URIToString(v URI) String { + return toString(v, uriToString, storage.EqualURI, uriFromString) +} + +// StringToBool creates a binding that connects a String data item to a Bool. +// Changes to the String will be parsed and pushed to the Bool if the parse was successful, and setting +// the Bool update the String binding. +// +// Since: 2.0 +func StringToBool(str String) Bool { + v := &fromStringTo[bool]{from: str, formatter: parseBool, parser: formatBool} + str.AddListener(v) + return v +} + +// StringToBoolWithFormat creates a binding that connects a String data item to a Bool and is +// presented using the specified format. Changes to the Bool will be parsed and if the format matches and +// the parse is successful it will be pushed to the String. Setting the Bool will push a formatted value +// into the String. +// +// Since: 2.0 +func StringToBoolWithFormat(str String, format string) Bool { + if format == "%t" { // Same as not using custom format. + return StringToBool(str) + } + + v := &fromStringTo[bool]{from: str, format: format} + str.AddListener(v) + return v +} + +// StringToFloat creates a binding that connects a String data item to a Float. +// Changes to the String will be parsed and pushed to the Float if the parse was successful, and setting +// the Float update the String binding. +// +// Since: 2.0 +func StringToFloat(str String) Float { + v := &fromStringTo[float64]{from: str, formatter: parseFloat, parser: formatFloat} + str.AddListener(v) + return v +} + +// StringToFloatWithFormat creates a binding that connects a String data item to a Float and is +// presented using the specified format. Changes to the Float will be parsed and if the format matches and +// the parse is successful it will be pushed to the String. Setting the Float will push a formatted value +// into the String. +// +// Since: 2.0 +func StringToFloatWithFormat(str String, format string) Float { + if format == "%f" { // Same as not using custom format. + return StringToFloat(str) + } + + v := &fromStringTo[float64]{from: str, format: format} + str.AddListener(v) + return v +} + +// StringToInt creates a binding that connects a String data item to a Int. +// Changes to the String will be parsed and pushed to the Int if the parse was successful, and setting +// the Int update the String binding. +// +// Since: 2.0 +func StringToInt(str String) Int { + v := &fromStringTo[int]{from: str, parser: formatInt, formatter: parseInt} + str.AddListener(v) + return v +} + +// StringToIntWithFormat creates a binding that connects a String data item to a Int and is +// presented using the specified format. Changes to the Int will be parsed and if the format matches and +// the parse is successful it will be pushed to the String. Setting the Int will push a formatted value +// into the String. +// +// Since: 2.0 +func StringToIntWithFormat(str String, format string) Int { + if format == "%d" { // Same as not using custom format. + return StringToInt(str) + } + + v := &fromStringTo[int]{from: str, format: format} + str.AddListener(v) + return v +} + +// StringToURI creates a binding that connects a String data item to a URI. +// Changes to the String will be parsed and pushed to the URI if the parse was successful, and setting +// the URI update the String binding. +// +// Since: 2.1 +func StringToURI(str String) URI { + v := &fromStringTo[fyne.URI]{from: str, parser: uriToString, formatter: uriFromString} + str.AddListener(v) + return v +} + +func toString[T any](v Item[T], formatter func(T) (string, error), comparator func(T, T) bool, parser func(string) (T, error)) *toStringFrom[T] { + str := &toStringFrom[T]{from: v, formatter: formatter, comparator: comparator, parser: parser} + v.AddListener(str) + return str +} + +func toStringComparable[T bool | float64 | int](v Item[T], formatter func(T) (string, error), parser func(string) (T, error)) *toStringFrom[T] { + return toString(v, formatter, func(t1, t2 T) bool { return t1 == t2 }, parser) +} + +func toStringWithFormat[T any](v Item[T], format, defaultFormat string, formatter func(T) (string, error), comparator func(T, T) bool, parser func(string) (T, error)) String { + str := toString(v, formatter, comparator, parser) + if format != defaultFormat { // Same as not using custom formatting. + str.format = format + } + + return str +} + +func toStringWithFormatComparable[T bool | float64 | int](v Item[T], format, defaultFormat string, formatter func(T) (string, error), parser func(string) (T, error)) String { + return toStringWithFormat(v, format, defaultFormat, formatter, func(t1, t2 T) bool { return t1 == t2 }, parser) +} + +type convertBaseItem struct { + base +} + +func (s *convertBaseItem) DataChanged() { + s.triggerFromMain() +} + +type toStringFrom[T any] struct { + convertBaseItem + + format string + + formatter func(T) (string, error) + comparator func(T, T) bool + parser func(string) (T, error) + + from Item[T] +} + +func (s *toStringFrom[T]) Get() (string, error) { + val, err := s.from.Get() + if err != nil { + return "", err + } + + if s.format != "" { + return fmt.Sprintf(s.format, val), nil + } + + return s.formatter(val) +} + +func (s *toStringFrom[T]) Set(str string) error { + var val T + if s.format != "" { + safe := stripFormatPrecision(s.format) + n, err := fmt.Sscanf(str, safe+" ", &val) // " " denotes match to end of string + if err != nil { + return err + } + if n != 1 { + return errParseFailed + } + } else { + new, err := s.parser(str) + if err != nil { + return err + } + val = new + } + + old, err := s.from.Get() + if err != nil { + return err + } + if s.comparator(val, old) { + return nil + } + if err = s.from.Set(val); err != nil { + return err + } + + s.trigger() + return nil +} + +type fromStringTo[T any] struct { + convertBaseItem + + format string + formatter func(string) (T, error) + parser func(T) (string, error) + + from String +} + +func (s *fromStringTo[T]) Get() (T, error) { + str, err := s.from.Get() + if str == "" || err != nil { + return *new(T), err + } + + var val T + if s.format != "" { + n, err := fmt.Sscanf(str, s.format+" ", &val) // " " denotes match to end of string + if err != nil { + return *new(T), err + } + if n != 1 { + return *new(T), errParseFailed + } + } else { + formatted, err := s.formatter(str) + if err != nil { + return *new(T), err + } + val = formatted + } + + return val, nil +} + +func (s *fromStringTo[T]) Set(val T) error { + var str string + if s.format != "" { + str = fmt.Sprintf(s.format, val) + } else { + parsed, err := s.parser(val) + if err != nil { + return err + } + str = parsed + } + + old, err := s.from.Get() + if str == old { + return err + } + + err = s.from.Set(str) + if err != nil { + return err + } + + s.trigger() + return nil +} + +type toInt[T float64] struct { + convertBaseItem + + formatter func(int) (T, error) + parser func(T) (int, error) + + from Item[T] +} + +func (s *toInt[T]) Get() (int, error) { + val, err := s.from.Get() + if err != nil { + return 0, err + } + return s.parser(val) +} + +func (s *toInt[T]) Set(v int) error { + val, err := s.formatter(v) + if err != nil { + return err + } + + old, err := s.from.Get() + if err != nil { + return err + } + if val == old { + return nil + } + err = s.from.Set(val) + if err != nil { + return err + } + + s.trigger() + return nil +} + +type fromIntTo[T float64] struct { + convertBaseItem + + formatter func(int) (T, error) + parser func(T) (int, error) + from Item[int] +} + +func (s *fromIntTo[T]) Get() (T, error) { + val, err := s.from.Get() + if err != nil { + return *new(T), err + } + return s.formatter(val) +} + +func (s *fromIntTo[T]) Set(val T) error { + i, err := s.parser(val) + if err != nil { + return err + } + old, err := s.from.Get() + if i == old { + return nil + } + if err != nil { + return err + } + err = s.from.Set(i) + if err != nil { + return err + } + + s.trigger() + return nil +} diff --git a/vendor/fyne.io/fyne/v2/data/binding/convert_helper.go b/vendor/fyne.io/fyne/v2/data/binding/convert_helper.go new file mode 100644 index 0000000..8f44416 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/data/binding/convert_helper.go @@ -0,0 +1,98 @@ +package binding + +import ( + "strconv" + "strings" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/storage" +) + +func stripFormatPrecision(in string) string { + // quick exit if certainly not float + if !strings.ContainsRune(in, 'f') { + return in + } + + start := -1 + end := -1 + runes := []rune(in) + for i, r := range runes { + switch r { + case '%': + if i > 0 && start == i-1 { // ignore %% + start = -1 + } else { + start = i + } + case 'f': + if start == -1 { // not part of format + continue + } + end = i + } + + if end > -1 { + break + } + } + if end == start+1 { // no width/precision + return in + } + + sizeRunes := runes[start+1 : end] + width, err := parseFloat(string(sizeRunes)) + if err != nil { + return string(runes[:start+1]) + string(runes[:end]) + } + + if sizeRunes[0] == '.' { // formats like %.2f + return string(runes[:start+1]) + string(runes[end:]) + } + return string(runes[:start+1]) + strconv.Itoa(int(width)) + string(runes[end:]) +} + +func uriFromString(in string) (fyne.URI, error) { + return storage.ParseURI(in) +} + +func uriToString(in fyne.URI) (string, error) { + if in == nil { + return "", nil + } + + return in.String(), nil +} + +func parseBool(in string) (bool, error) { + return strconv.ParseBool(in) +} + +func parseFloat(in string) (float64, error) { + return strconv.ParseFloat(in, 64) +} + +func parseInt(in string) (int, error) { + out, err := strconv.ParseInt(in, 0, 64) + return int(out), err +} + +func formatBool(in bool) (string, error) { + return strconv.FormatBool(in), nil +} + +func formatFloat(in float64) (string, error) { + return strconv.FormatFloat(in, 'f', 6, 64), nil +} + +func formatInt(in int) (string, error) { + return strconv.FormatInt(int64(in), 10), nil +} + +func internalFloatToInt(val float64) (int, error) { + return int(val), nil +} + +func internalIntToFloat(val int) (float64, error) { + return float64(val), nil +} diff --git a/vendor/fyne.io/fyne/v2/data/binding/items.go b/vendor/fyne.io/fyne/v2/data/binding/items.go new file mode 100644 index 0000000..e7081ea --- /dev/null +++ b/vendor/fyne.io/fyne/v2/data/binding/items.go @@ -0,0 +1,284 @@ +package binding + +import ( + "bytes" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/storage" +) + +// Item supports binding any type T generically. +// +// Since: 2.6 +type Item[T any] interface { + DataItem + Get() (T, error) + Set(T) error +} + +// ExternalItem supports binding any external value of type T. +// +// Since: 2.6 +type ExternalItem[T any] interface { + Item[T] + Reload() error +} + +// NewItem returns a bindable value of type T that is managed internally. +// +// Since: 2.6 +func NewItem[T any](comparator func(T, T) bool) Item[T] { + return &item[T]{val: new(T), comparator: comparator} +} + +// BindItem returns a new bindable value that controls the contents of the provided variable of type T. +// If your code changes the content of the variable this refers to you should call Reload() to inform the bindings. +// +// Since: 2.6 +func BindItem[T any](val *T, comparator func(T, T) bool) ExternalItem[T] { + if val == nil { + val = new(T) // never allow a nil value pointer + } + b := &externalItem[T]{} + b.comparator = comparator + b.val = val + b.old = *val + return b +} + +// Bool supports binding a bool value. +// +// Since: 2.0 +type Bool = Item[bool] + +// ExternalBool supports binding a bool value to an external value. +// +// Since: 2.0 +type ExternalBool = ExternalItem[bool] + +// NewBool returns a bindable bool value that is managed internally. +// +// Since: 2.0 +func NewBool() Bool { + return newItemComparable[bool]() +} + +// BindBool returns a new bindable value that controls the contents of the provided bool variable. +// If your code changes the content of the variable this refers to you should call Reload() to inform the bindings. +// +// Since: 2.0 +func BindBool(v *bool) ExternalBool { + return bindExternalComparable(v) +} + +// Bytes supports binding a []byte value. +// +// Since: 2.2 +type Bytes = Item[[]byte] + +// ExternalBytes supports binding a []byte value to an external value. +// +// Since: 2.2 +type ExternalBytes = ExternalItem[[]byte] + +// NewBytes returns a bindable []byte value that is managed internally. +// +// Since: 2.2 +func NewBytes() Bytes { + return NewItem(bytes.Equal) +} + +// BindBytes returns a new bindable value that controls the contents of the provided []byte variable. +// If your code changes the content of the variable this refers to you should call Reload() to inform the bindings. +// +// Since: 2.2 +func BindBytes(v *[]byte) ExternalBytes { + return BindItem(v, bytes.Equal) +} + +// Float supports binding a float64 value. +// +// Since: 2.0 +type Float = Item[float64] + +// ExternalFloat supports binding a float64 value to an external value. +// +// Since: 2.0 +type ExternalFloat = ExternalItem[float64] + +// NewFloat returns a bindable float64 value that is managed internally. +// +// Since: 2.0 +func NewFloat() Float { + return newItemComparable[float64]() +} + +// BindFloat returns a new bindable value that controls the contents of the provided float64 variable. +// If your code changes the content of the variable this refers to you should call Reload() to inform the bindings. +// +// Since: 2.0 +func BindFloat(v *float64) ExternalFloat { + return bindExternalComparable(v) +} + +// Int supports binding a int value. +// +// Since: 2.0 +type Int = Item[int] + +// ExternalInt supports binding a int value to an external value. +// +// Since: 2.0 +type ExternalInt = ExternalItem[int] + +// NewInt returns a bindable int value that is managed internally. +// +// Since: 2.0 +func NewInt() Int { + return newItemComparable[int]() +} + +// BindInt returns a new bindable value that controls the contents of the provided int variable. +// If your code changes the content of the variable this refers to you should call Reload() to inform the bindings. +// +// Since: 2.0 +func BindInt(v *int) ExternalInt { + return bindExternalComparable(v) +} + +// Rune supports binding a rune value. +// +// Since: 2.0 +type Rune = Item[rune] + +// ExternalRune supports binding a rune value to an external value. +// +// Since: 2.0 +type ExternalRune = ExternalItem[rune] + +// NewRune returns a bindable rune value that is managed internally. +// +// Since: 2.0 +func NewRune() Rune { + return newItemComparable[rune]() +} + +// BindRune returns a new bindable value that controls the contents of the provided rune variable. +// If your code changes the content of the variable this refers to you should call Reload() to inform the bindings. +// +// Since: 2.0 +func BindRune(v *rune) ExternalRune { + return bindExternalComparable(v) +} + +// String supports binding a string value. +// +// Since: 2.0 +type String = Item[string] + +// ExternalString supports binding a string value to an external value. +// +// Since: 2.0 +type ExternalString = ExternalItem[string] + +// NewString returns a bindable string value that is managed internally. +// +// Since: 2.0 +func NewString() String { + return newItemComparable[string]() +} + +// BindString returns a new bindable value that controls the contents of the provided string variable. +// If your code changes the content of the variable this refers to you should call Reload() to inform the bindings. +// +// Since: 2.0 +func BindString(v *string) ExternalString { + return bindExternalComparable(v) +} + +// URI supports binding a fyne.URI value. +// +// Since: 2.1 +type URI = Item[fyne.URI] + +// ExternalURI supports binding a fyne.URI value to an external value. +// +// Since: 2.1 +type ExternalURI = ExternalItem[fyne.URI] + +// NewURI returns a bindable fyne.URI value that is managed internally. +// +// Since: 2.1 +func NewURI() URI { + return NewItem(storage.EqualURI) +} + +// BindURI returns a new bindable value that controls the contents of the provided fyne.URI variable. +// If your code changes the content of the variable this refers to you should call Reload() to inform the bindings. +// +// Since: 2.1 +func BindURI(v *fyne.URI) ExternalURI { + return BindItem(v, storage.EqualURI) +} + +func newItemComparable[T bool | float64 | int | rune | string]() Item[T] { + return NewItem[T](func(a, b T) bool { return a == b }) +} + +type item[T any] struct { + base + + comparator func(T, T) bool + val *T +} + +func (b *item[T]) Get() (T, error) { + b.lock.RLock() + defer b.lock.RUnlock() + + if b.val == nil { + return *new(T), nil + } + return *b.val, nil +} + +func (b *item[T]) Set(val T) error { + b.lock.Lock() + equal := b.comparator(*b.val, val) + *b.val = val + b.lock.Unlock() + + if !equal { + b.trigger() + } + + return nil +} + +func bindExternalComparable[T bool | float64 | int | rune | string](val *T) ExternalItem[T] { + return BindItem(val, func(t1, t2 T) bool { return t1 == t2 }) +} + +type externalItem[T any] struct { + item[T] + + old T +} + +func (b *externalItem[T]) Set(val T) error { + b.lock.Lock() + if b.comparator(b.old, val) { + b.lock.Unlock() + return nil + } + *b.val = val + b.old = val + b.lock.Unlock() + + b.trigger() + return nil +} + +func (b *externalItem[T]) Reload() error { + return b.Set(*b.val) +} diff --git a/vendor/fyne.io/fyne/v2/data/binding/lists.go b/vendor/fyne.io/fyne/v2/data/binding/lists.go new file mode 100644 index 0000000..a9f6fb7 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/data/binding/lists.go @@ -0,0 +1,563 @@ +package binding + +import ( + "bytes" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/storage" +) + +// List supports binding a list of values with type T. +// +// Since: 2.7 +type List[T any] interface { + DataList + + Append(value T) error + Get() ([]T, error) + GetValue(index int) (T, error) + Prepend(value T) error + Remove(value T) error + Set(list []T) error + SetValue(index int, value T) error +} + +// ExternalList supports binding a list of values, with type T, from an external variable. +// +// Since: 2.7 +type ExternalList[T any] interface { + List[T] + + Reload() error +} + +// NewList returns a bindable list of values with type T. +// +// Since: 2.7 +func NewList[T any](comparator func(T, T) bool) List[T] { + return newList[T](comparator) +} + +// BindList returns a bound list of values with type T, based on the contents of the passed slice. +// If your code changes the content of the slice this refers to you should call Reload() to inform the bindings. +// +// Since: 2.7 +func BindList[T any](v *[]T, comparator func(T, T) bool) ExternalList[T] { + return bindList(v, comparator) +} + +// DataList is the base interface for all bindable data lists. +// +// Since: 2.0 +type DataList interface { + DataItem + GetItem(index int) (DataItem, error) + Length() int +} + +// BoolList supports binding a list of bool values. +// +// Since: 2.0 +type BoolList = List[bool] + +// ExternalBoolList supports binding a list of bool values from an external variable. +// +// Since: 2.0 +type ExternalBoolList = ExternalList[bool] + +// NewBoolList returns a bindable list of bool values. +// +// Since: 2.0 +func NewBoolList() List[bool] { + return newListComparable[bool]() +} + +// BindBoolList returns a bound list of bool values, based on the contents of the passed slice. +// If your code changes the content of the slice this refers to you should call Reload() to inform the bindings. +// +// Since: 2.0 +func BindBoolList(v *[]bool) ExternalList[bool] { + return bindListComparable(v) +} + +// BytesList supports binding a list of []byte values. +// +// Since: 2.2 +type BytesList = List[[]byte] + +// ExternalBytesList supports binding a list of []byte values from an external variable. +// +// Since: 2.2 +type ExternalBytesList = ExternalList[[]byte] + +// NewBytesList returns a bindable list of []byte values. +// +// Since: 2.2 +func NewBytesList() List[[]byte] { + return newList(bytes.Equal) +} + +// BindBytesList returns a bound list of []byte values, based on the contents of the passed slice. +// If your code changes the content of the slice this refers to you should call Reload() to inform the bindings. +// +// Since: 2.2 +func BindBytesList(v *[][]byte) ExternalList[[]byte] { + return bindList(v, bytes.Equal) +} + +// FloatList supports binding a list of float64 values. +// +// Since: 2.0 +type FloatList = List[float64] + +// ExternalFloatList supports binding a list of float64 values from an external variable. +// +// Since: 2.0 +type ExternalFloatList = ExternalList[float64] + +// NewFloatList returns a bindable list of float64 values. +// +// Since: 2.0 +func NewFloatList() List[float64] { + return newListComparable[float64]() +} + +// BindFloatList returns a bound list of float64 values, based on the contents of the passed slice. +// If your code changes the content of the slice this refers to you should call Reload() to inform the bindings. +// +// Since: 2.0 +func BindFloatList(v *[]float64) ExternalList[float64] { + return bindListComparable(v) +} + +// IntList supports binding a list of int values. +// +// Since: 2.0 +type IntList = List[int] + +// ExternalIntList supports binding a list of int values from an external variable. +// +// Since: 2.0 +type ExternalIntList = ExternalList[int] + +// NewIntList returns a bindable list of int values. +// +// Since: 2.0 +func NewIntList() List[int] { + return newListComparable[int]() +} + +// BindIntList returns a bound list of int values, based on the contents of the passed slice. +// If your code changes the content of the slice this refers to you should call Reload() to inform the bindings. +// +// Since: 2.0 +func BindIntList(v *[]int) ExternalList[int] { + return bindListComparable(v) +} + +// RuneList supports binding a list of rune values. +// +// Since: 2.0 +type RuneList = List[rune] + +// ExternalRuneList supports binding a list of rune values from an external variable. +// +// Since: 2.0 +type ExternalRuneList = ExternalList[rune] + +// NewRuneList returns a bindable list of rune values. +// +// Since: 2.0 +func NewRuneList() List[rune] { + return newListComparable[rune]() +} + +// BindRuneList returns a bound list of rune values, based on the contents of the passed slice. +// If your code changes the content of the slice this refers to you should call Reload() to inform the bindings. +// +// Since: 2.0 +func BindRuneList(v *[]rune) ExternalList[rune] { + return bindListComparable(v) +} + +// StringList supports binding a list of string values. +// +// Since: 2.0 +type StringList = List[string] + +// ExternalStringList supports binding a list of string values from an external variable. +// +// Since: 2.0 +type ExternalStringList = ExternalList[string] + +// NewStringList returns a bindable list of string values. +// +// Since: 2.0 +func NewStringList() List[string] { + return newListComparable[string]() +} + +// BindStringList returns a bound list of string values, based on the contents of the passed slice. +// If your code changes the content of the slice this refers to you should call Reload() to inform the bindings. +// +// Since: 2.0 +func BindStringList(v *[]string) ExternalList[string] { + return bindListComparable(v) +} + +// UntypedList supports binding a list of any values. +// +// Since: 2.1 +type UntypedList = List[any] + +// ExternalUntypedList supports binding a list of any values from an external variable. +// +// Since: 2.1 +type ExternalUntypedList = ExternalList[any] + +// NewUntypedList returns a bindable list of any values. +// +// Since: 2.1 +func NewUntypedList() List[any] { + return newList(func(t1, t2 any) bool { return t1 == t2 }) +} + +// BindUntypedList returns a bound list of any values, based on the contents of the passed slice. +// If your code changes the content of the slice this refers to you should call Reload() to inform the bindings. +// +// Since: 2.1 +func BindUntypedList(v *[]any) ExternalList[any] { + return bindList(v, func(t1, t2 any) bool { return t1 == t2 }) +} + +// URIList supports binding a list of fyne.URI values. +// +// Since: 2.1 +type URIList = List[fyne.URI] + +// ExternalURIList supports binding a list of fyne.URI values from an external variable. +// +// Since: 2.1 +type ExternalURIList = ExternalList[fyne.URI] + +// NewURIList returns a bindable list of fyne.URI values. +// +// Since: 2.1 +func NewURIList() List[fyne.URI] { + return newList(storage.EqualURI) +} + +// BindURIList returns a bound list of fyne.URI values, based on the contents of the passed slice. +// If your code changes the content of the slice this refers to you should call Reload() to inform the bindings. +// +// Since: 2.1 +func BindURIList(v *[]fyne.URI) ExternalList[fyne.URI] { + return bindList(v, storage.EqualURI) +} + +type listBase struct { + base + items []DataItem +} + +// GetItem returns the DataItem at the specified index. +func (b *listBase) GetItem(i int) (DataItem, error) { + b.lock.RLock() + defer b.lock.RUnlock() + + if i < 0 || i >= len(b.items) { + return nil, errOutOfBounds + } + + return b.items[i], nil +} + +// Length returns the number of items in this data list. +func (b *listBase) Length() int { + b.lock.RLock() + defer b.lock.RUnlock() + + return len(b.items) +} + +func (b *listBase) appendItem(i DataItem) { + b.items = append(b.items, i) +} + +func (b *listBase) deleteItem(i int) { + b.items = append(b.items[:i], b.items[i+1:]...) +} + +func newList[T any](comparator func(T, T) bool) *boundList[T] { + return &boundList[T]{val: new([]T), comparator: comparator} +} + +func newListComparable[T comparable]() *boundList[T] { + return newList(func(t1, t2 T) bool { return t1 == t2 }) +} + +func newExternalList[T any](v *[]T, comparator func(T, T) bool) *boundList[T] { + return &boundList[T]{val: v, comparator: comparator, updateExternal: true} +} + +func bindList[T any](v *[]T, comparator func(T, T) bool) *boundList[T] { + if v == nil { + return newList(comparator) + } + + l := newExternalList(v, comparator) + for i := range *v { + l.appendItem(bindListItem(v, i, l.updateExternal, comparator)) + } + + return l +} + +func bindListComparable[T comparable](v *[]T) *boundList[T] { + return bindList(v, func(t1, t2 T) bool { return t1 == t2 }) +} + +type boundList[T any] struct { + listBase + + comparator func(T, T) bool + updateExternal bool + val *[]T + + parentListener func(int) +} + +func (l *boundList[T]) Append(val T) error { + l.lock.Lock() + *l.val = append(*l.val, val) + + trigger, err := l.doReload() + l.lock.Unlock() + + if trigger { + l.trigger() + } + + return err +} + +func (l *boundList[T]) Get() ([]T, error) { + l.lock.RLock() + defer l.lock.RUnlock() + + return *l.val, nil +} + +func (l *boundList[T]) GetValue(i int) (T, error) { + l.lock.RLock() + defer l.lock.RUnlock() + + if i < 0 || i >= l.Length() { + return *new(T), errOutOfBounds + } + + return (*l.val)[i], nil +} + +func (l *boundList[T]) Prepend(val T) error { + l.lock.Lock() + *l.val = append([]T{val}, *l.val...) + + trigger, err := l.doReload() + l.lock.Unlock() + + if trigger { + l.trigger() + } + + return err +} + +func (l *boundList[T]) Reload() error { + l.lock.Lock() + trigger, err := l.doReload() + l.lock.Unlock() + + if trigger { + l.trigger() + } + + return err +} + +func (l *boundList[T]) Remove(val T) error { + l.lock.Lock() + + v := *l.val + if len(v) == 0 { + l.lock.Unlock() + return nil + } + if l.comparator(v[0], val) { + *l.val = v[1:] + } else if l.comparator(v[len(v)-1], val) { + *l.val = v[:len(v)-1] + } else { + id := -1 + for i, v := range v { + if l.comparator(v, val) { + id = i + break + } + } + + if id == -1 { + l.lock.Unlock() + return nil + } + *l.val = append(v[:id], v[id+1:]...) + } + + trigger, err := l.doReload() + l.lock.Unlock() + + if trigger { + l.trigger() + } + + return err +} + +func (l *boundList[T]) Set(v []T) error { + l.lock.Lock() + *l.val = v + trigger, err := l.doReload() + l.lock.Unlock() + + if trigger { + l.trigger() + } + + return err +} + +func (l *boundList[T]) doReload() (trigger bool, retErr error) { + oldLen := len(l.items) + newLen := len(*l.val) + if oldLen > newLen { + for i := oldLen - 1; i >= newLen; i-- { + l.deleteItem(i) + } + trigger = true + } else if oldLen < newLen { + for i := oldLen; i < newLen; i++ { + item := bindListItem(l.val, i, l.updateExternal, l.comparator) + + if l.parentListener != nil { + index := i + item.AddListener(NewDataListener(func() { + l.parentListener(index) + })) + } + + l.appendItem(item) + } + trigger = true + } + + for i, item := range l.items { + if i > oldLen || i > newLen { + break + } + + var err error + if l.updateExternal { + err = item.(*boundExternalListItem[T]).setIfChanged((*l.val)[i]) + } else { + err = item.(*boundListItem[T]).doSet((*l.val)[i]) + } + if err != nil { + retErr = err + } + } + return trigger, retErr +} + +func (l *boundList[T]) SetValue(i int, v T) error { + l.lock.RLock() + len := l.Length() + l.lock.RUnlock() + + if i < 0 || i >= len { + return errOutOfBounds + } + + l.lock.Lock() + (*l.val)[i] = v + l.lock.Unlock() + + item, err := l.GetItem(i) + if err != nil { + return err + } + return item.(Item[T]).Set(v) +} + +func bindListItem[T any](v *[]T, i int, external bool, comparator func(T, T) bool) Item[T] { + if external { + ret := &boundExternalListItem[T]{old: (*v)[i]} + ret.val = v + ret.index = i + ret.comparator = comparator + return ret + } + + return &boundListItem[T]{val: v, index: i, comparator: comparator} +} + +type boundListItem[T any] struct { + base + + comparator func(T, T) bool + val *[]T + index int +} + +func (b *boundListItem[T]) Get() (T, error) { + b.lock.Lock() + defer b.lock.Unlock() + + if b.index < 0 || b.index >= len(*b.val) { + return *new(T), errOutOfBounds + } + + return (*b.val)[b.index], nil +} + +func (b *boundListItem[T]) Set(val T) error { + return b.doSet(val) +} + +func (b *boundListItem[T]) doSet(val T) error { + b.lock.Lock() + (*b.val)[b.index] = val + b.lock.Unlock() + + b.trigger() + return nil +} + +type boundExternalListItem[T any] struct { + boundListItem[T] + + old T +} + +func (b *boundExternalListItem[T]) setIfChanged(val T) error { + b.lock.Lock() + if b.comparator(val, b.old) { + b.lock.Unlock() + return nil + } + (*b.val)[b.index] = val + b.old = val + + b.lock.Unlock() + b.trigger() + return nil +} diff --git a/vendor/fyne.io/fyne/v2/data/binding/maps.go b/vendor/fyne.io/fyne/v2/data/binding/maps.go new file mode 100644 index 0000000..43b8db1 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/data/binding/maps.go @@ -0,0 +1,411 @@ +package binding + +import ( + "errors" + "reflect" + + "fyne.io/fyne/v2" +) + +// DataMap is the base interface for all bindable data maps. +// +// Since: 2.0 +type DataMap interface { + DataItem + GetItem(string) (DataItem, error) + Keys() []string +} + +// ExternalUntypedMap is a map data binding with all values untyped (any), connected to an external data source. +// +// Since: 2.0 +type ExternalUntypedMap interface { + UntypedMap + Reload() error +} + +// UntypedMap is a map data binding with all values Untyped (any). +// +// Since: 2.0 +type UntypedMap interface { + DataMap + Delete(string) + Get() (map[string]any, error) + GetValue(string) (any, error) + Set(map[string]any) error + SetValue(string, any) error +} + +// NewUntypedMap creates a new, empty map binding of string to any. +// +// Since: 2.0 +func NewUntypedMap() UntypedMap { + return &mapBase{items: make(map[string]reflectUntyped), val: &map[string]any{}} +} + +// BindUntypedMap creates a new map binding of string to any based on the data passed. +// If your code changes the content of the map this refers to you should call Reload() to inform the bindings. +// +// Since: 2.0 +func BindUntypedMap(d *map[string]any) ExternalUntypedMap { + if d == nil { + return NewUntypedMap().(ExternalUntypedMap) + } + m := &mapBase{items: make(map[string]reflectUntyped), val: d, updateExternal: true} + + for k := range *d { + m.setItem(k, bindUntypedMapValue(d, k, m.updateExternal)) + } + + return m +} + +// Struct is the base interface for a bound struct type. +// +// Since: 2.0 +type Struct interface { + DataMap + GetValue(string) (any, error) + SetValue(string, any) error + Reload() error +} + +// BindStruct creates a new map binding of string to any using the struct passed as data. +// The key for each item is a string representation of each exported field with the value set as an any. +// Only exported fields are included. +// +// Since: 2.0 +func BindStruct(i any) Struct { + if i == nil { + return NewUntypedMap().(Struct) + } + t := reflect.TypeOf(i) + if t.Kind() != reflect.Ptr || + (reflect.TypeOf(reflect.ValueOf(i).Elem()).Kind() != reflect.Struct) { + fyne.LogError("Invalid type passed to BindStruct, must be pointer to struct", nil) + return NewUntypedMap().(Struct) + } + + s := &boundStruct{orig: i} + s.items = make(map[string]reflectUntyped) + s.val = &map[string]any{} + s.updateExternal = true + + v := reflect.ValueOf(i).Elem() + t = v.Type() + for j := 0; j < v.NumField(); j++ { + f := v.Field(j) + if !f.CanSet() { + continue + } + + key := t.Field(j).Name + s.items[key] = bindReflect(f) + (*s.val)[key] = f.Interface() + } + + return s +} + +type reflectUntyped interface { + DataItem + get() (any, error) + set(any) error +} + +type mapBase struct { + base + + updateExternal bool + items map[string]reflectUntyped + val *map[string]any +} + +func (b *mapBase) GetItem(key string) (DataItem, error) { + b.lock.RLock() + defer b.lock.RUnlock() + + if v, ok := b.items[key]; ok { + return v, nil + } + + return nil, errKeyNotFound +} + +func (b *mapBase) Keys() []string { + b.lock.Lock() + defer b.lock.Unlock() + + ret := make([]string, len(b.items)) + i := 0 + for k := range b.items { + ret[i] = k + i++ + } + + return ret +} + +func (b *mapBase) Delete(key string) { + b.lock.Lock() + defer b.lock.Unlock() + + delete(b.items, key) + + b.trigger() +} + +func (b *mapBase) Get() (map[string]any, error) { + b.lock.RLock() + defer b.lock.RUnlock() + + if b.val == nil { + return map[string]any{}, nil + } + + return *b.val, nil +} + +func (b *mapBase) GetValue(key string) (any, error) { + b.lock.RLock() + defer b.lock.RUnlock() + + if i, ok := b.items[key]; ok { + return i.get() + } + + return nil, errKeyNotFound +} + +func (b *mapBase) Reload() error { + b.lock.Lock() + defer b.lock.Unlock() + + return b.doReload() +} + +func (b *mapBase) Set(v map[string]any) error { + b.lock.Lock() + defer b.lock.Unlock() + + if b.val == nil { // was not initialized with a blank value, recover + b.val = &v + b.trigger() + return nil + } + + *b.val = v + return b.doReload() +} + +func (b *mapBase) SetValue(key string, d any) error { + b.lock.Lock() + defer b.lock.Unlock() + + if i, ok := b.items[key]; ok { + return i.set(d) + } + + (*b.val)[key] = d + item := bindUntypedMapValue(b.val, key, b.updateExternal) + b.setItem(key, item) + return nil +} + +func (b *mapBase) doReload() (retErr error) { + changed := false + // add new + for key := range *b.val { + _, found := b.items[key] + if !found { + b.setItem(key, bindUntypedMapValue(b.val, key, b.updateExternal)) + changed = true + } + } + + // remove old + for key := range b.items { + _, found := (*b.val)[key] + if !found { + delete(b.items, key) + changed = true + } + } + if changed { + b.trigger() + } + + for k, item := range b.items { + var err error + + if b.updateExternal { + err = item.(*boundExternalMapValue).setIfChanged((*b.val)[k]) + } else { + err = item.(*boundMapValue).set((*b.val)[k]) + } + + if err != nil { + retErr = err + } + } + return retErr +} + +func (b *mapBase) setItem(key string, d reflectUntyped) { + b.items[key] = d + + b.trigger() +} + +type boundStruct struct { + mapBase + + orig any +} + +func (b *boundStruct) Reload() (retErr error) { + b.lock.Lock() + defer b.lock.Unlock() + + v := reflect.ValueOf(b.orig).Elem() + t := v.Type() + for j := 0; j < v.NumField(); j++ { + f := v.Field(j) + if !f.CanSet() { + continue + } + kind := f.Kind() + if kind == reflect.Slice || kind == reflect.Struct { + fyne.LogError("Data binding does not yet support slice or struct elements in a struct", nil) + continue + } + + key := t.Field(j).Name + old := (*b.val)[key] + if f.Interface() == old { + continue + } + + var err error + switch kind { + case reflect.Bool: + err = b.items[key].(*boundReflect[bool]).Set(f.Bool()) + case reflect.Float32, reflect.Float64: + err = b.items[key].(*boundReflect[float64]).Set(f.Float()) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + err = b.items[key].(*boundReflect[int]).Set(int(f.Int())) + case reflect.String: + err = b.items[key].(*boundReflect[string]).Set(f.String()) + } + if err != nil { + retErr = err + } + (*b.val)[key] = f.Interface() + } + return retErr +} + +func bindUntypedMapValue(m *map[string]any, k string, external bool) reflectUntyped { + if external { + ret := &boundExternalMapValue{old: (*m)[k]} + ret.val = m + ret.key = k + return ret + } + + return &boundMapValue{val: m, key: k} +} + +type boundMapValue struct { + base + + val *map[string]any + key string +} + +func (b *boundMapValue) get() (any, error) { + if v, ok := (*b.val)[b.key]; ok { + return v, nil + } + + return nil, errKeyNotFound +} + +func (b *boundMapValue) set(val any) error { + (*b.val)[b.key] = val + + b.trigger() + return nil +} + +type boundExternalMapValue struct { + boundMapValue + + old any +} + +func (b *boundExternalMapValue) setIfChanged(val any) error { + if val == b.old { + return nil + } + b.old = val + + return b.set(val) +} + +type boundReflect[T any] struct { + base + + val reflect.Value +} + +func (b *boundReflect[T]) Get() (T, error) { + var zero T + val, err := b.get() + if err != nil { + return zero, err + } + + casted, ok := val.(T) + if !ok { + return zero, errors.New("unable to convert value to type") + } + + return casted, nil +} + +func (b *boundReflect[T]) Set(val T) error { + return b.set(val) +} + +func (b *boundReflect[T]) get() (any, error) { + if !b.val.CanInterface() { + return nil, errors.New("unable to get value from data binding") + } + + return b.val.Interface(), nil +} + +func (b *boundReflect[T]) set(val any) error { + if !b.val.CanSet() { + return errors.New("unable to set value in data binding") + } + + b.val.Set(reflect.ValueOf(val)) + b.trigger() + return nil +} + +func bindReflect(field reflect.Value) reflectUntyped { + switch field.Kind() { + case reflect.Bool: + return &boundReflect[bool]{val: field} + case reflect.Float32, reflect.Float64: + return &boundReflect[float64]{val: field} + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return &boundReflect[int]{val: field} + case reflect.String: + return &boundReflect[string]{val: field} + } + return &boundReflect[any]{val: field} +} diff --git a/vendor/fyne.io/fyne/v2/data/binding/pref_helper.go b/vendor/fyne.io/fyne/v2/data/binding/pref_helper.go new file mode 100644 index 0000000..972aaea --- /dev/null +++ b/vendor/fyne.io/fyne/v2/data/binding/pref_helper.go @@ -0,0 +1,97 @@ +package binding + +import ( + "sync" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/async" +) + +type preferenceItem interface { + checkForChange() +} + +type preferenceBindings struct { + async.Map[string, preferenceItem] +} + +func (b *preferenceBindings) list() []preferenceItem { + ret := []preferenceItem{} + b.Range(func(_ string, item preferenceItem) bool { + ret = append(ret, item) + return true + }) + return ret +} + +type preferencesMap struct { + prefs async.Map[fyne.Preferences, *preferenceBindings] + + appPrefs fyne.Preferences // the main application prefs, to check if it changed... + appLock sync.Mutex +} + +func newPreferencesMap() *preferencesMap { + return &preferencesMap{} +} + +func (m *preferencesMap) ensurePreferencesAttached(p fyne.Preferences) *preferenceBindings { + binds, loaded := m.prefs.LoadOrStore(p, &preferenceBindings{}) + if loaded { + return binds + } + + p.AddChangeListener(func() { m.preferencesChanged(p) }) + return binds +} + +func (m *preferencesMap) getBindings(p fyne.Preferences) *preferenceBindings { + if p == fyne.CurrentApp().Preferences() { + m.appLock.Lock() + prefs := m.appPrefs + if m.appPrefs == nil { + m.appPrefs = p + } + m.appLock.Unlock() + if prefs != p { + m.migratePreferences(prefs, p) + } + } + binds, _ := m.prefs.Load(p) + return binds +} + +func (m *preferencesMap) preferencesChanged(p fyne.Preferences) { + binds := m.getBindings(p) + if binds == nil { + return + } + for _, item := range binds.list() { + item.checkForChange() + } +} + +func (m *preferencesMap) migratePreferences(src, dst fyne.Preferences) { + old, loaded := m.prefs.Load(src) + if !loaded { + return + } + + m.prefs.Store(dst, old) + m.prefs.Delete(src) + m.appLock.Lock() + m.appPrefs = dst + m.appLock.Unlock() + + binds := m.getBindings(dst) + if binds == nil { + return + } + for _, b := range binds.list() { + if backed, ok := b.(interface{ replaceProvider(fyne.Preferences) }); ok { + backed.replaceProvider(dst) + } + } + + m.preferencesChanged(dst) +} diff --git a/vendor/fyne.io/fyne/v2/data/binding/preference.go b/vendor/fyne.io/fyne/v2/data/binding/preference.go new file mode 100644 index 0000000..e2600b4 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/data/binding/preference.go @@ -0,0 +1,259 @@ +package binding + +import ( + "sync/atomic" + + "fyne.io/fyne/v2" +) + +// Work around Go not supporting generic methods on non-generic types: +type preferenceLookupSetter[T any] func(fyne.Preferences) (func(string) T, func(string, T)) + +const keyTypeMismatchError = "A previous preference binding exists with different type for key: " + +// BindPreferenceBool returns a bindable bool value that is managed by the application preferences. +// Changes to this value will be saved to application storage and when the app starts the previous values will be read. +// +// Since: 2.0 +func BindPreferenceBool(key string, p fyne.Preferences) Bool { + return bindPreferenceItem(key, p, + func(p fyne.Preferences) (func(string) bool, func(string, bool)) { + return p.Bool, p.SetBool + }) +} + +// BindPreferenceBoolList returns a bound list of bool values that is managed by the application preferences. +// Changes to this value will be saved to application storage and when the app starts the previous values will be read. +// +// Since: 2.6 +func BindPreferenceBoolList(key string, p fyne.Preferences) BoolList { + return bindPreferenceListComparable(key, p, + func(p fyne.Preferences) (func(string) []bool, func(string, []bool)) { + return p.BoolList, p.SetBoolList + }, + ) +} + +// BindPreferenceFloat returns a bindable float64 value that is managed by the application preferences. +// Changes to this value will be saved to application storage and when the app starts the previous values will be read. +// +// Since: 2.0 +func BindPreferenceFloat(key string, p fyne.Preferences) Float { + return bindPreferenceItem(key, p, + func(p fyne.Preferences) (func(string) float64, func(string, float64)) { + return p.Float, p.SetFloat + }) +} + +// BindPreferenceFloatList returns a bound list of float64 values that is managed by the application preferences. +// Changes to this value will be saved to application storage and when the app starts the previous values will be read. +// +// Since: 2.6 +func BindPreferenceFloatList(key string, p fyne.Preferences) FloatList { + return bindPreferenceListComparable(key, p, + func(p fyne.Preferences) (func(string) []float64, func(string, []float64)) { + return p.FloatList, p.SetFloatList + }, + ) +} + +// BindPreferenceInt returns a bindable int value that is managed by the application preferences. +// Changes to this value will be saved to application storage and when the app starts the previous values will be read. +// +// Since: 2.0 +func BindPreferenceInt(key string, p fyne.Preferences) Int { + return bindPreferenceItem(key, p, + func(p fyne.Preferences) (func(string) int, func(string, int)) { + return p.Int, p.SetInt + }) +} + +// BindPreferenceIntList returns a bound list of int values that is managed by the application preferences. +// Changes to this value will be saved to application storage and when the app starts the previous values will be read. +// +// Since: 2.6 +func BindPreferenceIntList(key string, p fyne.Preferences) IntList { + return bindPreferenceListComparable(key, p, + func(p fyne.Preferences) (func(string) []int, func(string, []int)) { + return p.IntList, p.SetIntList + }, + ) +} + +// BindPreferenceString returns a bindable string value that is managed by the application preferences. +// Changes to this value will be saved to application storage and when the app starts the previous values will be read. +// +// Since: 2.0 +func BindPreferenceString(key string, p fyne.Preferences) String { + return bindPreferenceItem(key, p, + func(p fyne.Preferences) (func(string) string, func(string, string)) { + return p.String, p.SetString + }) +} + +// BindPreferenceStringList returns a bound list of string values that is managed by the application preferences. +// Changes to this value will be saved to application storage and when the app starts the previous values will be read. +// +// Since: 2.6 +func BindPreferenceStringList(key string, p fyne.Preferences) StringList { + return bindPreferenceListComparable(key, p, + func(p fyne.Preferences) (func(string) []string, func(string, []string)) { + return p.StringList, p.SetStringList + }, + ) +} + +func bindPreferenceItem[T bool | float64 | int | string](key string, p fyne.Preferences, setLookup preferenceLookupSetter[T]) Item[T] { + if found, ok := lookupExistingBinding[T](key, p); ok { + return found + } + + listen := &prefBoundBase[T]{key: key, setLookup: setLookup} + listen.replaceProvider(p) + binds := prefBinds.ensurePreferencesAttached(p) + binds.Store(key, listen) + return listen +} + +func lookupExistingBinding[T any](key string, p fyne.Preferences) (Item[T], bool) { + binds := prefBinds.getBindings(p) + if binds == nil { + return nil, false + } + + if listen, ok := binds.Load(key); listen != nil && ok { + if l, ok := listen.(Item[T]); ok { + return l, ok + } + fyne.LogError(keyTypeMismatchError+key, nil) + } + + return nil, false +} + +func lookupExistingListBinding[T bool | float64 | int | string](key string, p fyne.Preferences) (*prefBoundList[T], bool) { + binds := prefBinds.getBindings(p) + if binds == nil { + return nil, false + } + + if listen, ok := binds.Load(key); listen != nil && ok { + if l, ok := listen.(*prefBoundList[T]); ok { + return l, ok + } + fyne.LogError(keyTypeMismatchError+key, nil) + } + + return nil, false +} + +type prefBoundBase[T bool | float64 | int | string] struct { + base + key string + + get func(string) T + set func(string, T) + setLookup preferenceLookupSetter[T] + cache atomic.Pointer[T] +} + +func (b *prefBoundBase[T]) Get() (T, error) { + cache := b.get(b.key) + b.cache.Store(&cache) + return cache, nil +} + +func (b *prefBoundBase[T]) Set(v T) error { + b.set(b.key, v) + + b.lock.RLock() + defer b.lock.RUnlock() + b.trigger() + return nil +} + +func (b *prefBoundBase[T]) checkForChange() { + val := b.cache.Load() + if val != nil && b.get(b.key) == *val { + return + } + b.trigger() +} + +func (b *prefBoundBase[T]) replaceProvider(p fyne.Preferences) { + b.get, b.set = b.setLookup(p) +} + +type prefBoundList[T bool | float64 | int | string] struct { + boundList[T] + key string + + get func(string) []T + set func(string, []T) + setLookup preferenceLookupSetter[[]T] +} + +func (b *prefBoundList[T]) checkForChange() { + val := *b.val + updated := b.get(b.key) + if val == nil || len(updated) != len(val) { + b.Set(updated) + return + } + + // incoming changes to a preference list are not at the child level + for i, v := range val { + if i >= len(updated) { + break + } + + if !b.comparator(v, updated[i]) { + _ = b.items[i].(Item[T]).Set(updated[i]) + } + } +} + +func (b *prefBoundList[T]) replaceProvider(p fyne.Preferences) { + b.get, b.set = b.setLookup(p) +} + +type internalPrefs = interface{ WriteValues(func(map[string]any)) } + +func bindPreferenceListComparable[T bool | float64 | int | string](key string, p fyne.Preferences, + setLookup preferenceLookupSetter[[]T], +) *prefBoundList[T] { + if found, ok := lookupExistingListBinding[T](key, p); ok { + return found + } + + listen := &prefBoundList[T]{key: key, setLookup: setLookup} + listen.replaceProvider(p) + + items := listen.get(listen.key) + listen.boundList = *bindList(nil, func(t1, t2 T) bool { return t1 == t2 }) + + listen.boundList.AddListener(NewDataListener(func() { + cached := *listen.val + replaced := listen.get(listen.key) + if len(cached) == len(replaced) { + return + } + + listen.set(listen.key, *listen.val) + listen.trigger() + })) + + listen.boundList.parentListener = func(index int) { + listen.set(listen.key, *listen.val) + + // the child changes are not seen on the write end so force it + if prefs, ok := p.(internalPrefs); ok { + prefs.WriteValues(func(map[string]any) {}) + } + } + listen.boundList.Set(items) + + binds := prefBinds.ensurePreferencesAttached(p) + binds.Store(key, listen) + return listen +} diff --git a/vendor/fyne.io/fyne/v2/data/binding/sprintf.go b/vendor/fyne.io/fyne/v2/data/binding/sprintf.go new file mode 100644 index 0000000..ac9422f --- /dev/null +++ b/vendor/fyne.io/fyne/v2/data/binding/sprintf.go @@ -0,0 +1,218 @@ +package binding + +import ( + "fmt" + + "fyne.io/fyne/v2/storage" +) + +type sprintfString struct { + String + + format string + source []DataItem + err error +} + +// NewSprintf returns a String binding that format its content using the +// format string and the provide additional parameter that must be other +// data bindings. This data binding use fmt.Sprintf and fmt.Scanf internally +// and will have all the same limitation as those function. +// +// Since: 2.2 +func NewSprintf(format string, b ...DataItem) String { + ret := &sprintfString{ + String: NewString(), + format: format, + source: b, + } + + for _, value := range b { + value.AddListener(ret) + } + + return ret +} + +func (s *sprintfString) DataChanged() { + data := make([]any, 0, len(s.source)) + + s.err = nil + for _, value := range s.source { + switch x := value.(type) { + case Bool: + b, err := x.Get() + if err != nil { + s.err = err + return + } + + data = append(data, b) + case Bytes: + b, err := x.Get() + if err != nil { + s.err = err + return + } + + data = append(data, b) + case Float: + f, err := x.Get() + if err != nil { + s.err = err + return + } + + data = append(data, f) + case Int: + i, err := x.Get() + if err != nil { + s.err = err + return + } + + data = append(data, i) + case Rune: + r, err := x.Get() + if err != nil { + s.err = err + return + } + + data = append(data, r) + case String: + str, err := x.Get() + if err != nil { + s.err = err + // Set error? + return + } + + data = append(data, str) + case URI: + u, err := x.Get() + if err != nil { + s.err = err + return + } + + data = append(data, u) + } + } + + r := fmt.Sprintf(s.format, data...) + s.String.Set(r) +} + +func (s *sprintfString) Get() (string, error) { + if s.err != nil { + return "", s.err + } + return s.String.Get() +} + +func (s *sprintfString) Set(str string) error { + data := make([]any, 0, len(s.source)) + + s.err = nil + for _, value := range s.source { + switch value.(type) { + case Bool: + data = append(data, new(bool)) + case Bytes: + return fmt.Errorf("impossible to convert '%s' to []bytes type", str) + case Float: + data = append(data, new(float64)) + case Int: + data = append(data, new(int)) + case Rune: + data = append(data, new(rune)) + case String: + data = append(data, new(string)) + case URI: + data = append(data, new(string)) + } + } + + count, err := fmt.Sscanf(str, s.format, data...) + if err != nil { + return err + } + + if count != len(data) { + return fmt.Errorf("impossible to decode more than %v parameters in '%s' with format '%s'", count, str, s.format) + } + + for i, value := range s.source { + switch x := value.(type) { + case Bool: + v := data[i].(*bool) + + err := x.Set(*v) + if err != nil { + return err + } + case Bytes: + return fmt.Errorf("impossible to convert '%s' to []bytes type", str) + case Float: + v := data[i].(*float64) + + err := x.Set(*v) + if err != nil { + return err + } + case Int: + v := data[i].(*int) + + err := x.Set(*v) + if err != nil { + return err + } + case Rune: + v := data[i].(*rune) + + err := x.Set(*v) + if err != nil { + return err + } + case String: + v := data[i].(*string) + + err := x.Set(*v) + if err != nil { + return err + } + case URI: + v := data[i].(*string) + + if v == nil { + return fmt.Errorf("URI can not be nil in '%s'", str) + } + + uri, err := storage.ParseURI(*v) + if err != nil { + return err + } + + err = x.Set(uri) + if err != nil { + return err + } + } + } + + return nil +} + +// StringToStringWithFormat creates a binding that converts a string to another string using the specified format. +// Changes to the returned String will be pushed to the passed in String and setting a new string value will parse and +// set the underlying String if it matches the format and the parse was successful. +// +// Since: 2.2 +func StringToStringWithFormat(str String, format string) String { + if format == "%s" { // Same as not using custom formatting. + return str + } + + return NewSprintf(format, str) +} diff --git a/vendor/fyne.io/fyne/v2/data/binding/trees.go b/vendor/fyne.io/fyne/v2/data/binding/trees.go new file mode 100644 index 0000000..2c4ea2c --- /dev/null +++ b/vendor/fyne.io/fyne/v2/data/binding/trees.go @@ -0,0 +1,617 @@ +package binding + +import ( + "bytes" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/storage" +) + +// DataTreeRootID const is the value used as ID for the root of any tree binding. +const DataTreeRootID = "" + +// Tree supports binding a tree of values with type T. +// +// Since: 2.7 +type Tree[T any] interface { + DataTree + + Append(parent, id string, value T) error + Get() (map[string][]string, map[string]T, error) + GetValue(id string) (T, error) + Prepend(parent, id string, value T) error + Remove(id string) error + Set(ids map[string][]string, values map[string]T) error + SetValue(id string, value T) error +} + +// ExternalTree supports binding a tree of values, of type T, from an external variable. +// +// Since: 2.7 +type ExternalTree[T any] interface { + Tree[T] + + Reload() error +} + +// NewTree returns a bindable tree of values with type T. +// +// Since: 2.7 +func NewTree[T any](comparator func(T, T) bool) Tree[T] { + return newTree[T](comparator) +} + +// BindTree returns a bound tree of values with type T, based on the contents of the passed values. +// The ids map specifies how each item relates to its parent (with id ""), with the values being in the v map. +// If your code changes the content of the maps this refers to you should call Reload() to inform the bindings. +// +// Since: 2.7 +func BindTree[T any](ids *map[string][]string, v *map[string]T, comparator func(T, T) bool) ExternalTree[T] { + return bindTree(ids, v, comparator) +} + +// DataTree is the base interface for all bindable data trees. +// +// Since: 2.4 +type DataTree interface { + DataItem + GetItem(id string) (DataItem, error) + ChildIDs(string) []string +} + +// BoolTree supports binding a tree of bool values. +// +// Since: 2.4 +type BoolTree = Tree[bool] + +// ExternalBoolTree supports binding a tree of bool values from an external variable. +// +// Since: 2.4 +type ExternalBoolTree = ExternalTree[bool] + +// NewBoolTree returns a bindable tree of bool values. +// +// Since: 2.4 +func NewBoolTree() Tree[bool] { + return newTreeComparable[bool]() +} + +// BindBoolTree returns a bound tree of bool values, based on the contents of the passed values. +// The ids map specifies how each item relates to its parent (with id ""), with the values being in the v map. +// If your code changes the content of the maps this refers to you should call Reload() to inform the bindings. +// +// Since: 2.4 +func BindBoolTree(ids *map[string][]string, v *map[string]bool) ExternalTree[bool] { + return bindTreeComparable(ids, v) +} + +// BytesTree supports binding a tree of []byte values. +// +// Since: 2.4 +type BytesTree = Tree[[]byte] + +// ExternalBytesTree supports binding a tree of []byte values from an external variable. +// +// Since: 2.4 +type ExternalBytesTree = ExternalTree[[]byte] + +// NewBytesTree returns a bindable tree of []byte values. +// +// Since: 2.4 +func NewBytesTree() Tree[[]byte] { + return newTree(bytes.Equal) +} + +// BindBytesTree returns a bound tree of []byte values, based on the contents of the passed values. +// The ids map specifies how each item relates to its parent (with id ""), with the values being in the v map. +// If your code changes the content of the maps this refers to you should call Reload() to inform the bindings. +// +// Since: 2.4 +func BindBytesTree(ids *map[string][]string, v *map[string][]byte) ExternalTree[[]byte] { + return bindTree(ids, v, bytes.Equal) +} + +// FloatTree supports binding a tree of float64 values. +// +// Since: 2.4 +type FloatTree = Tree[float64] + +// ExternalFloatTree supports binding a tree of float64 values from an external variable. +// +// Since: 2.4 +type ExternalFloatTree = ExternalTree[float64] + +// NewFloatTree returns a bindable tree of float64 values. +// +// Since: 2.4 +func NewFloatTree() Tree[float64] { + return newTreeComparable[float64]() +} + +// BindFloatTree returns a bound tree of float64 values, based on the contents of the passed values. +// The ids map specifies how each item relates to its parent (with id ""), with the values being in the v map. +// If your code changes the content of the maps this refers to you should call Reload() to inform the bindings. +// +// Since: 2.4 +func BindFloatTree(ids *map[string][]string, v *map[string]float64) ExternalTree[float64] { + return bindTreeComparable(ids, v) +} + +// IntTree supports binding a tree of int values. +// +// Since: 2.4 +type IntTree = Tree[int] + +// ExternalIntTree supports binding a tree of int values from an external variable. +// +// Since: 2.4 +type ExternalIntTree = ExternalTree[int] + +// NewIntTree returns a bindable tree of int values. +// +// Since: 2.4 +func NewIntTree() Tree[int] { + return newTreeComparable[int]() +} + +// BindIntTree returns a bound tree of int values, based on the contents of the passed values. +// The ids map specifies how each item relates to its parent (with id ""), with the values being in the v map. +// If your code changes the content of the maps this refers to you should call Reload() to inform the bindings. +// +// Since: 2.4 +func BindIntTree(ids *map[string][]string, v *map[string]int) ExternalTree[int] { + return bindTreeComparable(ids, v) +} + +// RuneTree supports binding a tree of rune values. +// +// Since: 2.4 +type RuneTree = Tree[rune] + +// ExternalRuneTree supports binding a tree of rune values from an external variable. +// +// Since: 2.4 +type ExternalRuneTree = ExternalTree[rune] + +// NewRuneTree returns a bindable tree of rune values. +// +// Since: 2.4 +func NewRuneTree() Tree[rune] { + return newTreeComparable[rune]() +} + +// BindRuneTree returns a bound tree of rune values, based on the contents of the passed values. +// The ids map specifies how each item relates to its parent (with id ""), with the values being in the v map. +// If your code changes the content of the maps this refers to you should call Reload() to inform the bindings. +// +// Since: 2.4 +func BindRuneTree(ids *map[string][]string, v *map[string]rune) ExternalTree[rune] { + return bindTreeComparable(ids, v) +} + +// StringTree supports binding a tree of string values. +// +// Since: 2.4 +type StringTree = Tree[string] + +// ExternalStringTree supports binding a tree of string values from an external variable. +// +// Since: 2.4 +type ExternalStringTree = ExternalTree[string] + +// NewStringTree returns a bindable tree of string values. +// +// Since: 2.4 +func NewStringTree() Tree[string] { + return newTreeComparable[string]() +} + +// BindStringTree returns a bound tree of string values, based on the contents of the passed values. +// The ids map specifies how each item relates to its parent (with id ""), with the values being in the v map. +// If your code changes the content of the maps this refers to you should call Reload() to inform the bindings. +// +// Since: 2.4 +func BindStringTree(ids *map[string][]string, v *map[string]string) ExternalTree[string] { + return bindTreeComparable(ids, v) +} + +// UntypedTree supports binding a tree of any values. +// +// Since: 2.5 +type UntypedTree = Tree[any] + +// ExternalUntypedTree supports binding a tree of any values from an external variable. +// +// Since: 2.5 +type ExternalUntypedTree = ExternalTree[any] + +// NewUntypedTree returns a bindable tree of any values. +// +// Since: 2.5 +func NewUntypedTree() Tree[any] { + return newTree(func(a1, a2 any) bool { return a1 == a2 }) +} + +// BindUntypedTree returns a bound tree of any values, based on the contents of the passed values. +// The ids map specifies how each item relates to its parent (with id ""), with the values being in the v map. +// If your code changes the content of the maps this refers to you should call Reload() to inform the bindings. +// +// Since: 2.4 +func BindUntypedTree(ids *map[string][]string, v *map[string]any) ExternalTree[any] { + return bindTree(ids, v, func(a1, a2 any) bool { return a1 == a2 }) +} + +// URITree supports binding a tree of fyne.URI values. +// +// Since: 2.4 +type URITree = Tree[fyne.URI] + +// ExternalURITree supports binding a tree of fyne.URI values from an external variable. +// +// Since: 2.4 +type ExternalURITree = ExternalTree[fyne.URI] + +// NewURITree returns a bindable tree of fyne.URI values. +// +// Since: 2.4 +func NewURITree() Tree[fyne.URI] { + return newTree(storage.EqualURI) +} + +// BindURITree returns a bound tree of fyne.URI values, based on the contents of the passed values. +// The ids map specifies how each item relates to its parent (with id ""), with the values being in the v map. +// If your code changes the content of the maps this refers to you should call Reload() to inform the bindings. +// +// Since: 2.4 +func BindURITree(ids *map[string][]string, v *map[string]fyne.URI) ExternalTree[fyne.URI] { + return bindTree(ids, v, storage.EqualURI) +} + +type treeBase struct { + base + + ids map[string][]string + items map[string]DataItem +} + +// GetItem returns the DataItem at the specified id. +func (t *treeBase) GetItem(id string) (DataItem, error) { + t.lock.RLock() + defer t.lock.RUnlock() + + if item, ok := t.items[id]; ok { + return item, nil + } + + return nil, errOutOfBounds +} + +// ChildIDs returns the ordered IDs of items in this data tree that are children of the specified ID. +func (t *treeBase) ChildIDs(id string) []string { + t.lock.RLock() + defer t.lock.RUnlock() + + if ids, ok := t.ids[id]; ok { + return ids + } + + return []string{} +} + +func (t *treeBase) appendItem(i DataItem, id, parent string) { + t.items[id] = i + + ids := t.ids[parent] + for _, in := range ids { + if in == id { + return + } + } + t.ids[parent] = append(ids, id) +} + +func (t *treeBase) deleteItem(id, parent string) { + delete(t.items, id) + + ids, ok := t.ids[parent] + if !ok { + return + } + + off := -1 + for i, id2 := range ids { + if id2 == id { + off = i + break + } + } + if off == -1 { + return + } + t.ids[parent] = append(ids[:off], ids[off+1:]...) +} + +func parentIDFor(id string, ids map[string][]string) string { + for parent, list := range ids { + for _, child := range list { + if child == id { + return parent + } + } + } + + return "" +} + +func newTree[T any](comparator func(T, T) bool) *boundTree[T] { + t := &boundTree[T]{val: &map[string]T{}, comparator: comparator} + t.ids = make(map[string][]string) + t.items = make(map[string]DataItem) + return t +} + +func newTreeComparable[T comparable]() *boundTree[T] { + return newTree(func(t1, t2 T) bool { return t1 == t2 }) +} + +func bindTree[T any](ids *map[string][]string, v *map[string]T, comparator func(T, T) bool) *boundTree[T] { + if v == nil { + return newTree(comparator) + } + + t := &boundTree[T]{val: v, updateExternal: true, comparator: comparator} + t.ids = make(map[string][]string) + t.items = make(map[string]DataItem) + + for parent, children := range *ids { + for _, leaf := range children { + t.appendItem(bindTreeItem(v, leaf, t.updateExternal, t.comparator), leaf, parent) + } + } + + return t +} + +func bindTreeComparable[T comparable](ids *map[string][]string, v *map[string]T) *boundTree[T] { + return bindTree(ids, v, func(t1, t2 T) bool { return t1 == t2 }) +} + +type boundTree[T any] struct { + treeBase + + comparator func(T, T) bool + val *map[string]T + updateExternal bool +} + +func (t *boundTree[T]) Append(parent, id string, val T) error { + t.lock.Lock() + + t.ids[parent] = append(t.ids[parent], id) + v := *t.val + v[id] = val + + trigger, err := t.doReload() + t.lock.Unlock() + + if trigger { + t.trigger() + } + + return err +} + +func (t *boundTree[T]) Get() (map[string][]string, map[string]T, error) { + t.lock.RLock() + defer t.lock.RUnlock() + + return t.ids, *t.val, nil +} + +func (t *boundTree[T]) GetValue(id string) (T, error) { + t.lock.RLock() + defer t.lock.RUnlock() + + if item, ok := (*t.val)[id]; ok { + return item, nil + } + + return *new(T), errOutOfBounds +} + +func (t *boundTree[T]) Prepend(parent, id string, val T) error { + t.lock.Lock() + + t.ids[parent] = append([]string{id}, t.ids[parent]...) + v := *t.val + v[id] = val + + trigger, err := t.doReload() + t.lock.Unlock() + + if trigger { + t.trigger() + } + + return err +} + +func (t *boundTree[T]) Remove(id string) error { + t.lock.Lock() + t.removeChildren(id) + delete(t.ids, id) + v := *t.val + delete(v, id) + + trigger, err := t.doReload() + t.lock.Unlock() + + if trigger { + t.trigger() + } + + return err +} + +func (t *boundTree[T]) removeChildren(id string) { + for _, cid := range t.ids[id] { + t.removeChildren(cid) + + delete(t.ids, cid) + v := *t.val + delete(v, cid) + } +} + +func (t *boundTree[T]) Reload() error { + t.lock.Lock() + trigger, err := t.doReload() + t.lock.Unlock() + + if trigger { + t.trigger() + } + + return err +} + +func (t *boundTree[T]) Set(ids map[string][]string, v map[string]T) error { + t.lock.Lock() + t.ids = ids + *t.val = v + + trigger, err := t.doReload() + t.lock.Unlock() + + if trigger { + t.trigger() + } + + return err +} + +func (t *boundTree[T]) doReload() (fire bool, retErr error) { + updated := []string{} + for id := range *t.val { + found := false + for child := range t.items { + if child == id { // update existing + updated = append(updated, id) + found = true + break + } + } + if found { + continue + } + + // append new + t.appendItem(bindTreeItem(t.val, id, t.updateExternal, t.comparator), id, parentIDFor(id, t.ids)) + updated = append(updated, id) + fire = true + } + + for id := range t.items { + remove := true + for _, done := range updated { + if done == id { + remove = false + break + } + } + + if remove { // remove item no longer present + fire = true + t.deleteItem(id, parentIDFor(id, t.ids)) + } + } + + for id, item := range t.items { + var err error + if t.updateExternal { + err = item.(*boundExternalTreeItem[T]).setIfChanged((*t.val)[id]) + } else { + err = item.(*boundTreeItem[T]).doSet((*t.val)[id]) + } + if err != nil { + retErr = err + } + } + return fire, retErr +} + +func (t *boundTree[T]) SetValue(id string, v T) error { + t.lock.Lock() + (*t.val)[id] = v + t.lock.Unlock() + + item, err := t.GetItem(id) + if err != nil { + return err + } + return item.(Item[T]).Set(v) +} + +func bindTreeItem[T any](v *map[string]T, id string, external bool, comparator func(T, T) bool) Item[T] { + if external { + ret := &boundExternalTreeItem[T]{old: (*v)[id], comparator: comparator} + ret.val = v + ret.id = id + return ret + } + + return &boundTreeItem[T]{id: id, val: v} +} + +type boundTreeItem[T any] struct { + base + + val *map[string]T + id string +} + +func (t *boundTreeItem[T]) Get() (T, error) { + t.lock.Lock() + defer t.lock.Unlock() + + v := *t.val + if item, ok := v[t.id]; ok { + return item, nil + } + + return *new(T), errOutOfBounds +} + +func (t *boundTreeItem[T]) Set(val T) error { + return t.doSet(val) +} + +func (t *boundTreeItem[T]) doSet(val T) error { + t.lock.Lock() + (*t.val)[t.id] = val + t.lock.Unlock() + + t.trigger() + return nil +} + +type boundExternalTreeItem[T any] struct { + boundTreeItem[T] + + comparator func(T, T) bool + old T +} + +func (t *boundExternalTreeItem[T]) setIfChanged(val T) error { + t.lock.Lock() + if t.comparator(val, t.old) { + t.lock.Unlock() + return nil + } + (*t.val)[t.id] = val + t.old = val + t.lock.Unlock() + + t.trigger() + return nil +} diff --git a/vendor/fyne.io/fyne/v2/device.go b/vendor/fyne.io/fyne/v2/device.go new file mode 100644 index 0000000..b5dba46 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/device.go @@ -0,0 +1,44 @@ +package fyne + +// DeviceOrientation represents the different ways that a mobile device can be held +type DeviceOrientation int + +const ( + // OrientationVertical is the default vertical orientation + OrientationVertical DeviceOrientation = iota + // OrientationVerticalUpsideDown is the portrait orientation held upside down + OrientationVerticalUpsideDown + // OrientationHorizontalLeft is used to indicate a landscape orientation with the top to the left + OrientationHorizontalLeft + // OrientationHorizontalRight is used to indicate a landscape orientation with the top to the right + OrientationHorizontalRight +) + +// IsVertical is a helper utility that determines if a passed orientation is vertical +func IsVertical(orient DeviceOrientation) bool { + return orient == OrientationVertical || orient == OrientationVerticalUpsideDown +} + +// IsHorizontal is a helper utility that determines if a passed orientation is horizontal +func IsHorizontal(orient DeviceOrientation) bool { + return !IsVertical(orient) +} + +// Device provides information about the devices the code is running on +type Device interface { + Orientation() DeviceOrientation + IsMobile() bool + IsBrowser() bool + HasKeyboard() bool + SystemScaleForWindow(Window) float32 + + // Locale returns the information about this device's language and region. + // + // Since: 2.5 + Locale() Locale +} + +// CurrentDevice returns the device information for the current hardware (via the driver) +func CurrentDevice() Device { + return CurrentApp().Driver().Device() +} diff --git a/vendor/fyne.io/fyne/v2/dialog/base.go b/vendor/fyne.io/fyne/v2/dialog/base.go new file mode 100644 index 0000000..5d606ef --- /dev/null +++ b/vendor/fyne.io/fyne/v2/dialog/base.go @@ -0,0 +1,259 @@ +// Package dialog defines standard dialog windows for application GUIs. +package dialog // import "fyne.io/fyne/v2/dialog" + +import ( + "image/color" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + col "fyne.io/fyne/v2/internal/color" + "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" +) + +const ( + padWidth = 32 + padHeight = 16 +) + +// Dialog is the common API for any dialog window with a single dismiss button +type Dialog interface { + Show() + Hide() + SetDismissText(label string) + SetOnClosed(closed func()) + Refresh() + Resize(size fyne.Size) + + // MinSize returns the size that this dialog should not shrink below. + // + // Since: 2.1 + MinSize() fyne.Size + + // Dismiss instructs the dialog to close without any affirmative action. + // + // Since: 2.6 + Dismiss() +} + +// Declare conformity to Dialog interface +var _ Dialog = (*dialog)(nil) + +type dialog struct { + callback func(bool) + title string + icon fyne.Resource + desiredSize fyne.Size + + win *widget.PopUp + content fyne.CanvasObject + dismiss *widget.Button + parent fyne.Window + + // allows derived dialogs to inject logic that runs before Show() + beforeShowHook func() +} + +func (d *dialog) Dismiss() { + d.Hide() +} + +func (d *dialog) Hide() { + d.hideWithResponse(false) +} + +// MinSize returns the size that this dialog should not shrink below. +// +// Since: 2.1 +func (d *dialog) MinSize() fyne.Size { + return d.win.MinSize() +} + +func (d *dialog) Show() { + if d.beforeShowHook != nil { + d.beforeShowHook() + } + if !d.desiredSize.IsZero() { + d.win.Resize(d.desiredSize) + } + d.win.Show() +} + +func (d *dialog) Refresh() { + d.win.Refresh() +} + +// Resize dialog, call this function after dialog show +func (d *dialog) Resize(size fyne.Size) { + d.desiredSize = size + if d.win != nil { // could be called before popup is created! + d.win.Resize(size) + } +} + +// SetDismissText allows custom text to be set in the dismiss button +// This is a no-op for dialogs without dismiss buttons. +func (d *dialog) SetDismissText(label string) { + if d.dismiss == nil { + return + } + + d.dismiss.SetText(label) + d.win.Refresh() +} + +// SetOnClosed allows to set a callback function that is called when +// the dialog is closed +func (d *dialog) SetOnClosed(closed func()) { + // if there is already a callback set, remember it and call both + originalCallback := d.callback + + d.callback = func(response bool) { + if originalCallback != nil { + originalCallback(response) + } + closed() + } +} + +func (d *dialog) hideWithResponse(resp bool) { + d.win.Hide() + if d.callback != nil { + d.callback(resp) + } +} + +func (d *dialog) create(buttons fyne.CanvasObject) { + label := widget.NewLabelWithStyle(d.title, fyne.TextAlignLeading, fyne.TextStyle{Bold: true}) + + var image fyne.CanvasObject + if d.icon != nil { + image = &canvas.Image{Resource: d.icon} + } else { + image = &layout.Spacer{} + } + + content := container.New(&dialogLayout{d: d}, + image, + newThemedBackground(), + d.content, + buttons, + label, + ) + + d.win = widget.NewModalPopUp(content, d.parent.Canvas()) +} + +func (d *dialog) setButtons(buttons fyne.CanvasObject) { + d.win.Content.(*fyne.Container).Objects[3] = buttons + d.win.Refresh() +} + +func (d *dialog) setIcon(icon fyne.Resource) { + if icon == nil { + d.win.Content.(*fyne.Container).Objects[0] = &layout.Spacer{} + d.win.Refresh() + return + } + d.win.Content.(*fyne.Container).Objects[0] = &canvas.Image{Resource: icon} + d.win.Refresh() +} + +// The method .create() needs to be called before the dialog can be shown. +func newDialog(title, message string, icon fyne.Resource, callback func(bool), parent fyne.Window) *dialog { + d := &dialog{content: newCenterWrappedLabel(message), title: title, icon: icon, parent: parent} + d.callback = callback + + return d +} + +// =============================================================== +// ThemedBackground +// =============================================================== + +type themedBackground struct { + widget.BaseWidget +} + +func newThemedBackground() *themedBackground { + t := &themedBackground{} + t.ExtendBaseWidget(t) + return t +} + +func (t *themedBackground) CreateRenderer() fyne.WidgetRenderer { + t.ExtendBaseWidget(t) + rect := canvas.NewRectangle(theme.Color(theme.ColorNameOverlayBackground)) + return &themedBackgroundRenderer{rect, []fyne.CanvasObject{rect}} +} + +type themedBackgroundRenderer struct { + rect *canvas.Rectangle + objects []fyne.CanvasObject +} + +func (renderer *themedBackgroundRenderer) Destroy() { +} + +func (renderer *themedBackgroundRenderer) Layout(size fyne.Size) { + renderer.rect.Resize(size) +} + +func (renderer *themedBackgroundRenderer) MinSize() fyne.Size { + return renderer.rect.MinSize() +} + +func (renderer *themedBackgroundRenderer) Objects() []fyne.CanvasObject { + return renderer.objects +} + +func (renderer *themedBackgroundRenderer) Refresh() { + r, g, b, _ := col.ToNRGBA(theme.Color(theme.ColorNameOverlayBackground)) + bg := &color.NRGBA{R: uint8(r), G: uint8(g), B: uint8(b), A: 230} + renderer.rect.FillColor = bg +} + +// =============================================================== +// DialogLayout +// =============================================================== + +type dialogLayout struct { + d *dialog +} + +func (l *dialogLayout) Layout(obj []fyne.CanvasObject, size fyne.Size) { + btnMin := obj[3].MinSize() + labelMin := obj[4].MinSize() + + // icon + iconHeight := padHeight*2 + labelMin.Height*2 - theme.Padding() + obj[0].Resize(fyne.NewSize(iconHeight, iconHeight)) + obj[0].Move(fyne.NewPos(size.Width-iconHeight+theme.Padding(), -theme.Padding())) + + // background + obj[1].Move(fyne.NewPos(0, 0)) + obj[1].Resize(size) + + // content + contentStart := obj[4].Position().Y + labelMin.Height + padHeight + contentEnd := obj[3].Position().Y - theme.Padding() + obj[2].Move(fyne.NewPos(padWidth/2, labelMin.Height+padHeight)) + obj[2].Resize(fyne.NewSize(size.Width-padWidth, contentEnd-contentStart)) + + // buttons + obj[3].Resize(btnMin) + obj[3].Move(fyne.NewPos(size.Width/2-(btnMin.Width/2), size.Height-padHeight-btnMin.Height)) +} + +func (l *dialogLayout) MinSize(obj []fyne.CanvasObject) fyne.Size { + contentMin := obj[2].MinSize() + btnMin := obj[3].MinSize() + labelMin := obj[4].MinSize() + + width := fyne.Max(fyne.Max(contentMin.Width, btnMin.Width), labelMin.Width) + padWidth + height := contentMin.Height + btnMin.Height + labelMin.Height + theme.Padding() + padHeight*2 + + return fyne.NewSize(width, height) +} diff --git a/vendor/fyne.io/fyne/v2/dialog/color.go b/vendor/fyne.io/fyne/v2/dialog/color.go new file mode 100644 index 0000000..192937b --- /dev/null +++ b/vendor/fyne.io/fyne/v2/dialog/color.go @@ -0,0 +1,363 @@ +package dialog + +import ( + "fmt" + "image/color" + "math" + "math/cmplx" + "strings" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + col "fyne.io/fyne/v2/internal/color" + "fyne.io/fyne/v2/lang" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" +) + +const ( + checkeredBoxSize = 8 + checkeredNumberOfRings = 12 + + preferenceRecents = "color_recents" + preferenceMaxRecents = 7 +) + +// ColorPickerDialog is a simple dialog window that displays a color picker. +// +// Since: 1.4 +type ColorPickerDialog struct { + *dialog + Advanced bool + color color.Color + callback func(c color.Color) + advanced *widget.Accordion + picker *colorAdvancedPicker +} + +// NewColorPicker creates a color dialog and returns the handle. +// Using the returned type you should call Show() and then set its color through SetColor(). +// The callback is triggered when the user selects a color. +// +// Since: 1.4 +func NewColorPicker(title, message string, callback func(c color.Color), parent fyne.Window) *ColorPickerDialog { + return &ColorPickerDialog{ + dialog: newDialog(title, message, theme.ColorPaletteIcon(), nil /*cancel?*/, parent), + color: theme.Color(theme.ColorNamePrimary), + callback: callback, + } +} + +// ShowColorPicker creates and shows a color dialog. +// The callback is triggered when the user selects a color. +// +// Since: 1.4 +func ShowColorPicker(title, message string, callback func(c color.Color), parent fyne.Window) { + NewColorPicker(title, message, callback, parent).Show() +} + +// Refresh causes this dialog to be updated +func (p *ColorPickerDialog) Refresh() { + p.updateUI() +} + +// SetColor updates the color of the color picker. +func (p *ColorPickerDialog) SetColor(c color.Color) { + if p.picker == nil && p.Advanced { + p.updateUI() + } else if !p.Advanced { + fyne.LogError("Advanced mode needs to be enabled to use SetColor", nil) + return + } + p.picker.SetColor(c) +} + +// Show causes this dialog to be displayed +func (p *ColorPickerDialog) Show() { + if p.win == nil || p.Advanced != (p.advanced != nil) { + p.updateUI() + } + p.dialog.Show() +} + +func (p *ColorPickerDialog) createSimplePickers() (contents []fyne.CanvasObject) { + contents = append(contents, newColorBasicPicker(p.selectColor), newColorGreyscalePicker(p.selectColor)) + if recent := newColorRecentPicker(p.selectColor); len(recent.(*fyne.Container).Objects) > 0 { + // Add divider and recents if there are any, + contents = append(contents, canvas.NewLine(theme.Color(theme.ColorNameShadow)), recent) + } + return contents +} + +func (p *ColorPickerDialog) selectColor(c color.Color) { + writeRecentColor(colorToString(c)) + if p.picker != nil { + p.picker.SetColor(c) + } + if f := p.callback; f != nil { + f(c) + } + p.dialog.Hide() + p.updateUI() +} + +func (p *ColorPickerDialog) updateUI() { + if w := p.win; w != nil { + w.Hide() + } + p.dialog.dismiss = &widget.Button{ + Text: lang.L("Cancel"), Icon: theme.CancelIcon(), + OnTapped: p.dialog.Hide, + } + if p.Advanced { + p.picker = newColorAdvancedPicker(p.color, func(c color.Color) { + p.color = c + }) + + advancedItem := widget.NewAccordionItem(lang.L("Advanced"), p.picker) + if p.advanced != nil { + advancedItem.Open = p.advanced.Items[0].Open + } + p.advanced = widget.NewAccordion(advancedItem) + + p.dialog.content = container.NewVBox( + container.NewCenter( + container.NewVBox( + p.createSimplePickers()..., + ), + ), + widget.NewSeparator(), + p.advanced, + ) + + confirm := &widget.Button{ + Text: lang.L("Confirm"), Icon: theme.ConfirmIcon(), Importance: widget.HighImportance, + OnTapped: func() { + p.selectColor(p.color) + }, + } + p.dialog.create(container.NewGridWithColumns(2, p.dialog.dismiss, confirm)) + } else { + p.dialog.content = container.NewVBox(p.createSimplePickers()...) + p.dialog.create(container.NewGridWithColumns(1, p.dialog.dismiss)) + } +} + +func clamp(value, min, max int) int { + if value < min { + return min + } + if value > max { + return max + } + return value +} + +func wrapHue(hue int) int { + for hue < 0 { + hue += 360 + } + for hue > 360 { + hue -= 360 + } + return hue +} + +func newColorButtonBox(colors []color.Color, icon fyne.Resource, callback func(color.Color)) fyne.CanvasObject { + var objects []fyne.CanvasObject + if icon != nil && len(colors) > 0 { + objects = append(objects, widget.NewIcon(icon)) + } + for _, c := range colors { + objects = append(objects, newColorButton(c, callback)) + } + return container.NewGridWithColumns(8, objects...) +} + +func newCheckeredBackground(radial bool) *canvas.Raster { + f := func(x, y, _, _ int) color.Color { + if (x/checkeredBoxSize)%2 == (y/checkeredBoxSize)%2 { + return color.Gray{Y: 58} + } + + return color.Gray{Y: 84} + } + + if radial { + rect := f + f = func(x, y, w, h int) color.Color { + r, t := cmplx.Polar(complex(float64(x)-float64(w)/2, float64(y)-float64(h)/2)) + limit := math.Min(float64(w), float64(h)) / 2.0 + if r > limit { + // Out of bounds + return &color.NRGBA{A: 0} + } + + x = int((t + math.Pi) / (2 * math.Pi) * checkeredNumberOfRings * checkeredBoxSize) + y = int(r) + return rect(x, y, 0, 0) + } + } + + return canvas.NewRasterWithPixels(f) +} + +func readRecentColors() (recents []string) { + for _, r := range strings.Split(fyne.CurrentApp().Preferences().String(preferenceRecents), ",") { + if r != "" { + recents = append(recents, r) + } + } + return recents +} + +func writeRecentColor(color string) { + recents := []string{color} + for _, r := range readRecentColors() { + if r == color { + continue // Color already in recents + } + recents = append(recents, r) + } + if len(recents) > preferenceMaxRecents { + recents = recents[:preferenceMaxRecents] + } + fyne.CurrentApp().Preferences().SetString(preferenceRecents, strings.Join(recents, ",")) +} + +func colorToString(c color.Color) string { + red, green, blue, alpha := col.ToNRGBA(c) + if alpha == 0xff { + return fmt.Sprintf("#%02x%02x%02x", red, green, blue) + } + return fmt.Sprintf("#%02x%02x%02x%02x", red, green, blue, alpha) +} + +func stringToColor(s string) (color.Color, error) { + var c color.NRGBA + var err error + if len(s) == 7 { + c.A = 0xFF + _, err = fmt.Sscanf(s, "#%02x%02x%02x", &c.R, &c.G, &c.B) + } else { + _, err = fmt.Sscanf(s, "#%02x%02x%02x%02x", &c.R, &c.G, &c.B, &c.A) + } + return c, err +} + +func stringsToColors(ss ...string) (colors []color.Color) { + for _, s := range ss { + if s == "" { + continue + } + c, err := stringToColor(s) + if err != nil { + fyne.LogError("Couldn't parse color:", err) + } else { + colors = append(colors, c) + } + } + return colors +} + +func colorToHSLA(c color.Color) (int, int, int, int) { + r, g, b, a := col.ToNRGBA(c) + h, s, l := rgbToHsl(r, g, b) + return h, s, l, a +} + +// https://www.niwa.nu/2013/05/math-behind-colorspace-conversions-rgb-hsl/ + +func rgbToHsl(r, g, b int) (int, int, int) { + red := float64(r) / 255.0 + green := float64(g) / 255.0 + blue := float64(b) / 255.0 + + min := math.Min(red, math.Min(green, blue)) + max := math.Max(red, math.Max(green, blue)) + + lightness := (max + min) / 2.0 + + delta := max - min + + if delta == 0.0 { + // Achromatic + return 0, 0, int(lightness * 100.0) + } + + // Chromatic + + var saturation float64 + + if lightness < 0.5 { + saturation = (max - min) / (max + min) + } else { + saturation = (max - min) / (2.0 - max - min) + } + + var hue float64 + + if red == max { + hue = (green - blue) / delta + } else if green == max { + hue = 2.0 + (blue-red)/delta + } else if blue == max { + hue = 4.0 + (red-green)/delta + } + + h := wrapHue(int(hue * 60.0)) + s := int(saturation * 100.0) + l := int(lightness * 100.0) + return h, s, l +} + +func hslToRgb(h, s, l int) (int, int, int) { + hue := float64(h) / 360.0 + saturation := float64(s) / 100.0 + lightness := float64(l) / 100.0 + + if saturation == 0.0 { + // Greyscale + g := int(lightness * 255.0) + return g, g, g + } + + var v1 float64 + if lightness < 0.5 { + v1 = lightness * (1.0 + saturation) + } else { + v1 = (lightness + saturation) - (lightness * saturation) + } + + v2 := 2.0*lightness - v1 + + red := hueToChannel(hue+(1.0/3.0), v1, v2) + green := hueToChannel(hue, v1, v2) + blue := hueToChannel(hue-(1.0/3.0), v1, v2) + + r := int(math.Round(255.0 * red)) + g := int(math.Round(255.0 * green)) + b := int(math.Round(255.0 * blue)) + + return r, g, b +} + +func hueToChannel(h, v1, v2 float64) float64 { + for h < 0.0 { + h += 1.0 + } + for h > 1.0 { + h -= 1.0 + } + if 6.0*h < 1.0 { + return v2 + (v1-v2)*6*h + } + if 2.0*h < 1.0 { + return v1 + } + if 3.0*h < 2.0 { + return v2 + (v1-v2)*6*((2.0/3.0)-h) + } + return v2 +} diff --git a/vendor/fyne.io/fyne/v2/dialog/color_button.go b/vendor/fyne.io/fyne/v2/dialog/color_button.go new file mode 100644 index 0000000..1cd5cf1 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/dialog/color_button.go @@ -0,0 +1,116 @@ +package dialog + +import ( + "image/color" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/driver/desktop" + internalwidget "fyne.io/fyne/v2/internal/widget" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" +) + +var ( + _ fyne.Widget = (*colorButton)(nil) + _ desktop.Hoverable = (*colorButton)(nil) +) + +// colorButton displays a color and triggers the callback when tapped. +type colorButton struct { + widget.BaseWidget + color color.Color + onTap func(color.Color) + hovered bool +} + +// newColorButton creates a colorButton with the given color and callback. +func newColorButton(color color.Color, onTap func(color.Color)) *colorButton { + b := &colorButton{ + color: color, + onTap: onTap, + } + b.ExtendBaseWidget(b) + return b +} + +// CreateRenderer is a private method to Fyne which links this widget to its renderer +func (b *colorButton) CreateRenderer() fyne.WidgetRenderer { + b.ExtendBaseWidget(b) + background := newCheckeredBackground(false) + rectangle := &canvas.Rectangle{ + FillColor: b.color, + } + return &colorButtonRenderer{ + BaseRenderer: internalwidget.NewBaseRenderer([]fyne.CanvasObject{background, rectangle}), + button: b, + background: background, + rectangle: rectangle, + } +} + +// MouseIn is called when a desktop pointer enters the widget +func (b *colorButton) MouseIn(*desktop.MouseEvent) { + b.hovered = true + b.Refresh() +} + +// MouseOut is called when a desktop pointer exits the widget +func (b *colorButton) MouseOut() { + b.hovered = false + b.Refresh() +} + +// MouseMoved is called when a desktop pointer hovers over the widget +func (b *colorButton) MouseMoved(*desktop.MouseEvent) { +} + +// MinSize returns the size that this widget should not shrink below +func (b *colorButton) MinSize() fyne.Size { + return b.BaseWidget.MinSize() +} + +// SetColor updates the color selected in this color widget +func (b *colorButton) SetColor(color color.Color) { + if b.color == color { + return + } + b.color = color + b.Refresh() +} + +// Tapped is called when a pointer tapped event is captured and triggers any change handler +func (b *colorButton) Tapped(*fyne.PointEvent) { + if f := b.onTap; f != nil { + f(b.color) + } +} + +type colorButtonRenderer struct { + internalwidget.BaseRenderer + button *colorButton + background *canvas.Raster + rectangle *canvas.Rectangle +} + +func (r *colorButtonRenderer) Layout(size fyne.Size) { + r.rectangle.Move(fyne.NewPos(0, 0)) + r.rectangle.Resize(size) + r.background.Resize(size) +} + +func (r *colorButtonRenderer) MinSize() fyne.Size { + return r.rectangle.MinSize().Max(fyne.NewSize(32, 32)) +} + +func (r *colorButtonRenderer) Refresh() { + if r.button.hovered { + r.rectangle.StrokeColor = theme.Color(theme.ColorNameHover) + r.rectangle.StrokeWidth = theme.Padding() + } else { + r.rectangle.StrokeWidth = 0 + } + r.rectangle.FillColor = r.button.color + r.background.Refresh() + canvas.Refresh(r.button) +} diff --git a/vendor/fyne.io/fyne/v2/dialog/color_channel.go b/vendor/fyne.io/fyne/v2/dialog/color_channel.go new file mode 100644 index 0000000..d16974a --- /dev/null +++ b/vendor/fyne.io/fyne/v2/dialog/color_channel.go @@ -0,0 +1,187 @@ +package dialog + +import ( + "strconv" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + internalwidget "fyne.io/fyne/v2/internal/widget" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" +) + +var _ fyne.Widget = (*colorChannel)(nil) + +// colorChannel controls a channel of a color and triggers the callback when changed. +type colorChannel struct { + widget.BaseWidget + name string + min, max int + value int + onChanged func(int) +} + +// newColorChannel returns a new color channel control for the channel with the given name. +func newColorChannel(name string, min, max, value int, onChanged func(int)) *colorChannel { + c := &colorChannel{ + name: name, + min: min, + max: max, + value: clamp(value, min, max), + onChanged: onChanged, + } + c.ExtendBaseWidget(c) + return c +} + +// CreateRenderer is a private method to Fyne which links this widget to its renderer +func (c *colorChannel) CreateRenderer() fyne.WidgetRenderer { + label := widget.NewLabelWithStyle(c.name, fyne.TextAlignTrailing, fyne.TextStyle{Bold: true}) + entry := newColorChannelEntry(c) + slider := &widget.Slider{ + Value: 0.0, + Min: float64(c.min), + Max: float64(c.max), + Step: 1.0, + Orientation: widget.Horizontal, + OnChanged: func(value float64) { + c.SetValue(int(value)) + }, + } + r := &colorChannelRenderer{ + BaseRenderer: internalwidget.NewBaseRenderer([]fyne.CanvasObject{ + label, + slider, + entry, + }), + control: c, + label: label, + entry: entry, + slider: slider, + } + r.updateObjects() + return r +} + +// MinSize returns the size that this widget should not shrink below +func (c *colorChannel) MinSize() fyne.Size { + c.ExtendBaseWidget(c) + return c.BaseWidget.MinSize() +} + +// SetValue updates the value in this color widget +func (c *colorChannel) SetValue(value int) { + value = clamp(value, c.min, c.max) + if c.value == value { + return + } + c.value = value + c.Refresh() + if f := c.onChanged; f != nil { + f(value) + } +} + +type colorChannelRenderer struct { + internalwidget.BaseRenderer + control *colorChannel + label *widget.Label + entry *colorChannelEntry + slider *widget.Slider +} + +func (r *colorChannelRenderer) Layout(size fyne.Size) { + lMin := r.label.MinSize() + eMin := r.entry.MinSize() + r.label.Move(fyne.NewPos(0, (size.Height-lMin.Height)/2)) + r.label.Resize(fyne.NewSize(lMin.Width, lMin.Height)) + r.slider.Move(fyne.NewPos(lMin.Width, 0)) + r.slider.Resize(fyne.NewSize(size.Width-lMin.Width-eMin.Width, size.Height)) + r.entry.Move(fyne.NewPos(size.Width-eMin.Width, 0)) + r.entry.Resize(fyne.NewSize(eMin.Width, size.Height)) +} + +func (r *colorChannelRenderer) MinSize() fyne.Size { + lMin := r.label.MinSize() + sMin := r.slider.MinSize() + eMin := r.entry.MinSize() + return fyne.NewSize( + lMin.Width+sMin.Width+eMin.Width, + fyne.Max(lMin.Height, fyne.Max(sMin.Height, eMin.Height)), + ) +} + +func (r *colorChannelRenderer) Refresh() { + r.updateObjects() + r.Layout(r.control.Size()) + canvas.Refresh(r.control) +} + +func (r *colorChannelRenderer) updateObjects() { + r.entry.SetText(strconv.Itoa(r.control.value)) + r.slider.Value = float64(r.control.value) + r.slider.Refresh() +} + +type colorChannelEntry struct { + userChangeEntry +} + +func newColorChannelEntry(c *colorChannel) *colorChannelEntry { + e := &colorChannelEntry{} + e.Text = "0" + e.ExtendBaseWidget(e) + e.setOnChanged(func(text string) { + value, err := strconv.Atoi(text) + if err != nil { + fyne.LogError("Couldn't parse: "+text, err) + return + } + c.SetValue(value) + }) + return e +} + +func (e *colorChannelEntry) MinSize() fyne.Size { + // Ensure space for 3 digits + min := fyne.MeasureText("000", theme.TextSize(), fyne.TextStyle{}) + min = min.Add(fyne.NewSize(theme.Padding()*6, theme.Padding()*4)) + return min.Max(e.Entry.MinSize()) +} + +type userChangeEntry struct { + widget.Entry + userTyped bool +} + +func newUserChangeEntry(text string) *userChangeEntry { + e := &userChangeEntry{} + e.Entry.Text = text + e.ExtendBaseWidget(e) + return e +} + +func (e *userChangeEntry) setOnChanged(onChanged func(s string)) { + e.Entry.OnChanged = func(text string) { + if !e.userTyped { + return + } + + e.userTyped = false + + if onChanged != nil { + onChanged(text) + } + } + e.ExtendBaseWidget(e) +} + +func (e *userChangeEntry) TypedRune(r rune) { + e.userTyped = true + e.Entry.TypedRune(r) +} + +func (e *userChangeEntry) TypedKey(ev *fyne.KeyEvent) { + e.userTyped = true + e.Entry.TypedKey(ev) +} diff --git a/vendor/fyne.io/fyne/v2/dialog/color_picker.go b/vendor/fyne.io/fyne/v2/dialog/color_picker.go new file mode 100644 index 0000000..7040e86 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/dialog/color_picker.go @@ -0,0 +1,304 @@ +package dialog + +import ( + "image/color" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + col "fyne.io/fyne/v2/internal/color" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" +) + +// newColorBasicPicker returns a component for selecting basic colors. +func newColorBasicPicker(callback func(color.Color)) fyne.CanvasObject { + return newColorButtonBox( + stringsToColors( + "#f44336", // red + "#ff9800", // orange + "#ffeb3b", // yellow + "#8bc34a", // green + "#296ff6", // blue + "#9c27b0", // purple + "#795548", // brown + ), + theme.ColorChromaticIcon(), + callback, + ) +} + +// newColorGreyscalePicker returns a component for selecting greyscale colors. +func newColorGreyscalePicker(callback func(color.Color)) fyne.CanvasObject { + return newColorButtonBox( + stringsToColors( + "#ffffff", + "#cccccc", + "#aaaaaa", + "#808080", + "#555555", + "#333333", + "#000000", + ), + theme.ColorAchromaticIcon(), + callback, + ) +} + +// newColorRecentPicker returns a component for selecting recent colors. +func newColorRecentPicker(callback func(color.Color)) fyne.CanvasObject { + return newColorButtonBox(stringsToColors(readRecentColors()...), theme.HistoryIcon(), callback) +} + +var _ fyne.Widget = (*colorAdvancedPicker)(nil) + +// colorAdvancedPicker widget is a component for selecting a color. +type colorAdvancedPicker struct { + widget.BaseWidget + Red, Green, Blue, Alpha int // Range 0-255 + Hue int // Range 0-360 (degrees) + Saturation, Lightness int // Range 0-100 (percent) + ColorModel string + previousColor color.Color + + onChange func(color.Color) +} + +// newColorAdvancedPicker returns a new color widget set to the given color. +func newColorAdvancedPicker(color color.Color, onChange func(color.Color)) *colorAdvancedPicker { + c := &colorAdvancedPicker{ + onChange: onChange, + } + c.ExtendBaseWidget(c) + c.previousColor = color + c.updateColor(color) + return c +} + +// Color returns the currently selected color. +func (p *colorAdvancedPicker) Color() color.Color { + return &color.NRGBA{ + uint8(p.Red), + uint8(p.Green), + uint8(p.Blue), + uint8(p.Alpha), + } +} + +// SetColor updates the color selected in this color widget. +func (p *colorAdvancedPicker) SetColor(color color.Color) { + p.previousColor = color + if p.updateColor(color) { + p.Refresh() + if f := p.onChange; f != nil { + f(color) + } + } +} + +// SetHSLA updated the Hue, Saturation, Lightness, and Alpha components of the currently selected color. +func (p *colorAdvancedPicker) SetHSLA(h, s, l, a int) { + if p.updateHSLA(h, s, l, a) { + p.Refresh() + if f := p.onChange; f != nil { + f(p.Color()) + } + } +} + +// SetRGBA updated the Red, Green, Blue, and Alpha components of the currently selected color. +func (p *colorAdvancedPicker) SetRGBA(r, g, b, a int) { + if p.updateRGBA(r, g, b, a) { + p.Refresh() + if f := p.onChange; f != nil { + f(p.Color()) + } + } +} + +// MinSize returns the size that this widget should not shrink below. +func (p *colorAdvancedPicker) MinSize() fyne.Size { + p.ExtendBaseWidget(p) + return p.BaseWidget.MinSize() +} + +// CreateRenderer is a private method to Fyne which links this widget to its renderer. +func (p *colorAdvancedPicker) CreateRenderer() fyne.WidgetRenderer { + p.ExtendBaseWidget(p) + + // Preview + preview := newColorPreview(p.previousColor) + + // HSL + hueChannel := newColorChannel("H", 0, 360, p.Hue, func(h int) { + p.SetHSLA(h, p.Saturation, p.Lightness, p.Alpha) + }) + saturationChannel := newColorChannel("S", 0, 100, p.Saturation, func(s int) { + p.SetHSLA(p.Hue, s, p.Lightness, p.Alpha) + }) + lightnessChannel := newColorChannel("L", 0, 100, p.Lightness, func(l int) { + p.SetHSLA(p.Hue, p.Saturation, l, p.Alpha) + }) + hslBox := container.NewVBox( + hueChannel, + saturationChannel, + lightnessChannel, + ) + + // RGB + redChannel := newColorChannel("R", 0, 255, p.Red, func(r int) { + p.SetRGBA(r, p.Green, p.Blue, p.Alpha) + }) + greenChannel := newColorChannel("G", 0, 255, p.Green, func(g int) { + p.SetRGBA(p.Red, g, p.Blue, p.Alpha) + }) + blueChannel := newColorChannel("B", 0, 255, p.Blue, func(b int) { + p.SetRGBA(p.Red, p.Green, b, p.Alpha) + }) + rgbBox := container.NewVBox( + redChannel, + greenChannel, + blueChannel, + ) + + // Wheel + wheel := newColorWheel(func(hue, saturation, lightness, alpha int) { + p.SetHSLA(hue, saturation, lightness, alpha) + }) + + // Alpha + alphaChannel := newColorChannel("A", 0, 255, p.Alpha, func(a int) { + p.SetRGBA(p.Red, p.Green, p.Blue, a) + }) + + // Hex + hex := newUserChangeEntry("") + hex.setOnChanged(func(text string) { + c, err := stringToColor(text) + if err != nil { + fyne.LogError("Error parsing color: "+text, err) + // TODO trigger entry invalid state + } else { + p.SetColor(c) + } + }) + + contents := container.NewPadded(container.NewVBox( + container.NewGridWithColumns(3, + container.NewPadded(wheel), + hslBox, + rgbBox), + container.NewGridWithColumns(3, + container.NewPadded(preview), + + hex, + alphaChannel, + ), + )) + + r := &colorPickerRenderer{ + WidgetRenderer: widget.NewSimpleRenderer(contents), + picker: p, + redChannel: redChannel, + greenChannel: greenChannel, + blueChannel: blueChannel, + hueChannel: hueChannel, + saturationChannel: saturationChannel, + lightnessChannel: lightnessChannel, + wheel: wheel, + preview: preview, + alphaChannel: alphaChannel, + hex: hex, + contents: contents, + } + r.updateObjects() + return r +} + +func (p *colorAdvancedPicker) updateColor(color color.Color) bool { + r, g, b, a := col.ToNRGBA(color) + if p.Red == r && p.Green == g && p.Blue == b && p.Alpha == a { + return false + } + return p.updateRGBA(r, g, b, a) +} + +func (p *colorAdvancedPicker) updateHSLA(h, s, l, a int) bool { + h = wrapHue(h) + s = clamp(s, 0, 100) + l = clamp(l, 0, 100) + a = clamp(a, 0, 255) + if p.Hue == h && p.Saturation == s && p.Lightness == l && p.Alpha == a { + return false + } + p.Hue = h + p.Saturation = s + p.Lightness = l + p.Alpha = a + p.Red, p.Green, p.Blue = hslToRgb(p.Hue, p.Saturation, p.Lightness) + return true +} + +func (p *colorAdvancedPicker) updateRGBA(r, g, b, a int) bool { + r = clamp(r, 0, 255) + g = clamp(g, 0, 255) + b = clamp(b, 0, 255) + a = clamp(a, 0, 255) + if p.Red == r && p.Green == g && p.Blue == b && p.Alpha == a { + return false + } + p.Red = r + p.Green = g + p.Blue = b + p.Alpha = a + p.Hue, p.Saturation, p.Lightness = rgbToHsl(p.Red, p.Green, p.Blue) + return true +} + +var _ fyne.WidgetRenderer = (*colorPickerRenderer)(nil) + +type colorPickerRenderer struct { + fyne.WidgetRenderer + picker *colorAdvancedPicker + redChannel *colorChannel + greenChannel *colorChannel + blueChannel *colorChannel + hueChannel *colorChannel + saturationChannel *colorChannel + lightnessChannel *colorChannel + wheel *colorWheel + preview *colorPreview + alphaChannel *colorChannel + hex *userChangeEntry + contents fyne.CanvasObject +} + +func (r *colorPickerRenderer) Refresh() { + r.updateObjects() + r.WidgetRenderer.Refresh() +} + +func (r *colorPickerRenderer) updateObjects() { + // HSL + r.hueChannel.SetValue(r.picker.Hue) + r.saturationChannel.SetValue(r.picker.Saturation) + r.lightnessChannel.SetValue(r.picker.Lightness) + + // RGB + r.redChannel.SetValue(r.picker.Red) + r.greenChannel.SetValue(r.picker.Green) + r.blueChannel.SetValue(r.picker.Blue) + + // Wheel + r.wheel.SetHSLA(r.picker.Hue, r.picker.Saturation, r.picker.Lightness, r.picker.Alpha) + + color := r.picker.Color() + + // Preview + r.preview.SetColor(color) + + // Alpha + r.alphaChannel.SetValue(r.picker.Alpha) + + // Hex + r.hex.SetText(colorToString(color)) +} diff --git a/vendor/fyne.io/fyne/v2/dialog/color_preview.go b/vendor/fyne.io/fyne/v2/dialog/color_preview.go new file mode 100644 index 0000000..860040e --- /dev/null +++ b/vendor/fyne.io/fyne/v2/dialog/color_preview.go @@ -0,0 +1,78 @@ +package dialog + +import ( + "image/color" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + internalwidget "fyne.io/fyne/v2/internal/widget" + "fyne.io/fyne/v2/widget" +) + +// colorPreview displays a 2 part rectangle showing the current and previous selected colours +type colorPreview struct { + widget.BaseWidget + + previous, current color.Color +} + +func newColorPreview(previousColor color.Color) *colorPreview { + p := &colorPreview{previous: previousColor} + + p.ExtendBaseWidget(p) + return p +} + +// CreateRenderer is a private method to Fyne which links this widget to its renderer. +func (p *colorPreview) CreateRenderer() fyne.WidgetRenderer { + oldC := canvas.NewRectangle(p.previous) + newC := canvas.NewRectangle(p.current) + background := newCheckeredBackground(false) + return &colorPreviewRenderer{ + BaseRenderer: internalwidget.NewBaseRenderer([]fyne.CanvasObject{background, oldC, newC}), + preview: p, + background: background, + old: oldC, + new: newC, + } +} + +func (p *colorPreview) SetColor(c color.Color) { + p.current = c + p.Refresh() +} + +func (p *colorPreview) MinSize() fyne.Size { + p.ExtendBaseWidget(p) + return p.BaseWidget.MinSize() +} + +type colorPreviewRenderer struct { + internalwidget.BaseRenderer + preview *colorPreview + background *canvas.Raster + old, new *canvas.Rectangle +} + +func (r *colorPreviewRenderer) Layout(size fyne.Size) { + s := fyne.NewSize(size.Width/2, size.Height) + r.background.Resize(size) + r.old.Resize(s) + r.new.Resize(s) + r.new.Move(fyne.NewPos(s.Width, 0)) +} + +func (r *colorPreviewRenderer) MinSize() fyne.Size { + s := r.old.MinSize() + s.Width *= 2 + return s.Max(fyne.NewSize(16, 8)) +} + +func (r *colorPreviewRenderer) Refresh() { + r.background.Refresh() + + r.old.FillColor = r.preview.previous + r.old.Refresh() + r.new.FillColor = r.preview.current + r.new.Refresh() +} diff --git a/vendor/fyne.io/fyne/v2/dialog/color_wheel.go b/vendor/fyne.io/fyne/v2/dialog/color_wheel.go new file mode 100644 index 0000000..7a09943 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/dialog/color_wheel.go @@ -0,0 +1,212 @@ +package dialog + +import ( + "image" + "image/color" + "image/draw" + "math" + "math/cmplx" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/driver/desktop" + internalwidget "fyne.io/fyne/v2/internal/widget" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" +) + +var ( + _ fyne.Widget = (*colorWheel)(nil) + _ fyne.Tappable = (*colorWheel)(nil) + _ fyne.Draggable = (*colorWheel)(nil) +) + +// colorWheel displays a circular color gradient and triggers the callback when tapped. +type colorWheel struct { + widget.BaseWidget + generator func(w, h int) image.Image + cache draw.Image + onChange func(int, int, int, int) + + Hue int // Range 0-360 (degrees) + Saturation, Lightness int // Range 0-100 (percent) + Alpha int // Range 0-255 +} + +// newColorWheel returns a new color area that triggers the given onChange callback when tapped. +func newColorWheel(onChange func(int, int, int, int)) *colorWheel { + a := &colorWheel{ + onChange: onChange, + } + a.generator = func(w, h int) image.Image { + if a.cache == nil || a.cache.Bounds().Dx() != w || a.cache.Bounds().Dy() != h { + rect := image.Rect(0, 0, w, h) + a.cache = image.NewRGBA(rect) + } + for x := 0; x < w; x++ { + for y := 0; y < h; y++ { + if c := a.colorAt(x, y, w, h); c != nil { + a.cache.Set(x, y, c) + } + } + } + return a.cache + } + a.ExtendBaseWidget(a) + return a +} + +// Cursor returns the cursor type of this widget. +func (a *colorWheel) Cursor() desktop.Cursor { + return desktop.CrosshairCursor +} + +// CreateRenderer is a private method to Fyne which links this widget to its renderer. +func (a *colorWheel) CreateRenderer() fyne.WidgetRenderer { + raster := &canvas.Raster{ + Generator: a.generator, + } + background := newCheckeredBackground(true) + x := canvas.NewLine(color.Black) + y := canvas.NewLine(color.Black) + return &colorWheelRenderer{ + BaseRenderer: internalwidget.NewBaseRenderer([]fyne.CanvasObject{background, raster, x, y}), + area: a, + background: background, + raster: raster, + x: x, + y: y, + } +} + +// MinSize returns the size that this widget should not shrink below. +func (a *colorWheel) MinSize() fyne.Size { + a.ExtendBaseWidget(a) + return a.BaseWidget.MinSize() +} + +// SetHSLA updates the selected color in the wheel. +func (a *colorWheel) SetHSLA(hue, saturation, lightness, alpha int) { + if a.Hue == hue && a.Saturation == saturation && a.Lightness == lightness && a.Alpha == alpha { + return + } + a.Hue = hue + a.Saturation = saturation + a.Lightness = lightness + a.Alpha = alpha + a.Refresh() +} + +// Tapped is called when a pointer tapped event is captured and triggers any change handler. +func (a *colorWheel) Tapped(event *fyne.PointEvent) { + a.trigger(event.Position) +} + +// Dragged is called when a pointer drag event is captured and triggers any change handler +func (a *colorWheel) Dragged(event *fyne.DragEvent) { + a.trigger(event.Position) +} + +// DragEnd is called when a pointer drag ends +func (a *colorWheel) DragEnd() { +} + +func (a *colorWheel) colorAt(x, y, w, h int) color.Color { + width, height := float64(w), float64(h) + dx := float64(x) - (width / 2.0) + dy := float64(y) - (height / 2.0) + radius, radians := cmplx.Polar(complex(dx, dy)) + limit := math.Min(width, height) / 2.0 + if radius > limit { + // Out of bounds + return color.Transparent + } + degrees := radians * (180.0 / math.Pi) + hue := wrapHue(int(degrees)) + saturation := int(radius / limit * 100.0) + red, green, blue := hslToRgb(hue, saturation, a.Lightness) + return &color.NRGBA{ + R: uint8(red), + G: uint8(green), + B: uint8(blue), + A: uint8(a.Alpha), + } +} + +func (a *colorWheel) locationForPosition(pos fyne.Position) (x, y int) { + can := fyne.CurrentApp().Driver().CanvasForObject(a) + x, y = int(pos.X), int(pos.Y) + if can != nil { + x, y = can.PixelCoordinateForPosition(pos) + } + return x, y +} + +func (a *colorWheel) selection(width, height float32) (float32, float32) { + w, h := float64(width), float64(height) + radius := float64(a.Saturation) / 100.0 * math.Min(w, h) / 2.0 + degrees := float64(a.Hue) + radians := degrees * math.Pi / 180.0 + c := cmplx.Rect(radius, radians) + return float32(real(c) + w/2.0), float32(imag(c) + h/2.0) +} + +func (a *colorWheel) trigger(pos fyne.Position) { + x, y := a.locationForPosition(pos) + if c, f := a.cache, a.onChange; c != nil && f != nil { + b := c.Bounds() + width, height := float64(b.Dx()), float64(b.Dy()) + dx := float64(x) - (width / 2) + dy := float64(y) - (height / 2) + radius, radians := cmplx.Polar(complex(dx, dy)) + limit := math.Min(width, height) / 2.0 + if radius > limit { + // Out of bounds + return + } + degrees := radians * (180.0 / math.Pi) + a.Hue = wrapHue(int(degrees)) + a.Saturation = int(radius / limit * 100.0) + f(a.Hue, a.Saturation, a.Lightness, a.Alpha) + } + a.Refresh() +} + +type colorWheelRenderer struct { + internalwidget.BaseRenderer + area *colorWheel + background *canvas.Raster + raster *canvas.Raster + x, y *canvas.Line +} + +func (r *colorWheelRenderer) Layout(size fyne.Size) { + x, y := r.area.selection(size.Width, size.Height) + r.x.Position1 = fyne.NewPos(0, y) + r.x.Position2 = fyne.NewPos(size.Width, y) + r.y.Position1 = fyne.NewPos(x, 0) + r.y.Position2 = fyne.NewPos(x, size.Height) + r.raster.Move(fyne.NewPos(0, 0)) + r.raster.Resize(size) + r.background.Resize(size) +} + +func (r *colorWheelRenderer) MinSize() fyne.Size { + return r.raster.MinSize().Max(fyne.NewSize(128, 128)) +} + +func (r *colorWheelRenderer) Refresh() { + s := r.area.Size() + if s.IsZero() { + r.area.Resize(r.area.MinSize()) + } else { + r.Layout(s) + } + r.x.StrokeColor = theme.Color(theme.ColorNameForeground) + r.x.Refresh() + r.y.StrokeColor = theme.Color(theme.ColorNameForeground) + r.y.Refresh() + r.raster.Refresh() + r.background.Refresh() + canvas.Refresh(r.area) +} diff --git a/vendor/fyne.io/fyne/v2/dialog/confirm.go b/vendor/fyne.io/fyne/v2/dialog/confirm.go new file mode 100644 index 0000000..726449d --- /dev/null +++ b/vendor/fyne.io/fyne/v2/dialog/confirm.go @@ -0,0 +1,65 @@ +package dialog + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/lang" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" +) + +// ConfirmDialog is like the standard Dialog but with an additional confirmation button +type ConfirmDialog struct { + *dialog + + confirm *widget.Button +} + +// Confirm instructs the dialog to close agreeing with whatever content was displayed. +// +// Since: 2.6 +func (d *ConfirmDialog) Confirm() { + d.hideWithResponse(true) +} + +// SetConfirmText allows custom text to be set in the confirmation button +func (d *ConfirmDialog) SetConfirmText(label string) { + d.confirm.SetText(label) + d.win.Refresh() +} + +// SetConfirmImportance sets the importance level of the confirm button. +// +// Since 2.4 +func (d *ConfirmDialog) SetConfirmImportance(importance widget.Importance) { + d.confirm.Importance = importance +} + +// NewConfirm creates a dialog over the specified window for user confirmation. +// The title is used for the dialog window and message is the content. +// The callback is executed when the user decides. After creation you should call Show(). +func NewConfirm(title, message string, callback func(bool), parent fyne.Window) *ConfirmDialog { + d := newTextDialog(title, message, theme.QuestionIcon(), parent) + d.callback = callback + + d.dismiss = &widget.Button{ + Text: lang.L("No"), Icon: theme.CancelIcon(), + OnTapped: d.Hide, + } + confirm := &widget.Button{ + Text: lang.L("Yes"), Icon: theme.ConfirmIcon(), Importance: widget.HighImportance, + OnTapped: func() { + d.hideWithResponse(true) + }, + } + d.create(container.NewGridWithColumns(2, d.dismiss, confirm)) + + return &ConfirmDialog{dialog: d, confirm: confirm} +} + +// ShowConfirm shows a dialog over the specified window for a user +// confirmation. The title is used for the dialog window and message is the content. +// The callback is executed when the user decides. +func ShowConfirm(title, message string, callback func(bool), parent fyne.Window) { + NewConfirm(title, message, callback, parent).Show() +} diff --git a/vendor/fyne.io/fyne/v2/dialog/custom.go b/vendor/fyne.io/fyne/v2/dialog/custom.go new file mode 100644 index 0000000..f54ddb1 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/dialog/custom.go @@ -0,0 +1,107 @@ +package dialog + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" +) + +var _ Dialog = (*CustomDialog)(nil) + +// CustomDialog implements a custom dialog. +// +// Since: 2.4 +type CustomDialog struct { + *dialog +} + +// NewCustom creates and returns a dialog over the specified application using custom +// content. The button will have the dismiss text set. +// The MinSize() of the CanvasObject passed will be used to set the size of the window. +func NewCustom(title, dismiss string, content fyne.CanvasObject, parent fyne.Window) *CustomDialog { + d := &dialog{content: content, title: title, parent: parent} + + d.dismiss = &widget.Button{Text: dismiss, OnTapped: d.Hide} + d.create(container.NewGridWithColumns(1, d.dismiss)) + + return &CustomDialog{dialog: d} +} + +// ShowCustom shows a dialog over the specified application using custom +// content. The button will have the dismiss text set. +// The MinSize() of the CanvasObject passed will be used to set the size of the window. +func ShowCustom(title, dismiss string, content fyne.CanvasObject, parent fyne.Window) { + NewCustom(title, dismiss, content, parent).Show() +} + +// NewCustomWithoutButtons creates a new custom dialog without any buttons. +// The MinSize() of the CanvasObject passed will be used to set the size of the window. +// +// Since: 2.4 +func NewCustomWithoutButtons(title string, content fyne.CanvasObject, parent fyne.Window) *CustomDialog { + d := &dialog{content: content, title: title, parent: parent} + d.create(container.NewGridWithColumns(1)) + + return &CustomDialog{dialog: d} +} + +// SetButtons sets the row of buttons at the bottom of the dialog. +// Passing an empty slice will result in a dialog with no buttons. +// +// Since: 2.4 +func (d *CustomDialog) SetButtons(buttons []fyne.CanvasObject) { + d.dismiss = nil // New button row invalidates possible dismiss button. + d.setButtons(container.NewGridWithRows(1, buttons...)) +} + +// SetIcon sets an icon to be shown in the top right of the dialog. +// Passing a nil resource will remove the icon from the dialog. +// +// Since: 2.6 +func (d *CustomDialog) SetIcon(icon fyne.Resource) { + d.setIcon(icon) +} + +// ShowCustomWithoutButtons shows a dialog, without buttons, over the specified application +// using custom content. +// The MinSize() of the CanvasObject passed will be used to set the size of the window. +// +// Since: 2.4 +func ShowCustomWithoutButtons(title string, content fyne.CanvasObject, parent fyne.Window) { + NewCustomWithoutButtons(title, content, parent).Show() +} + +// NewCustomConfirm creates and returns a dialog over the specified application using +// custom content. The cancel button will have the dismiss text set and the "OK" will +// use the confirm text. The response callback is called on user action. +// The MinSize() of the CanvasObject passed will be used to set the size of the window. +func NewCustomConfirm(title, confirm, dismiss string, content fyne.CanvasObject, + callback func(bool), parent fyne.Window, +) *ConfirmDialog { + d := &dialog{content: content, title: title, parent: parent, callback: callback} + + d.dismiss = &widget.Button{ + Text: dismiss, Icon: theme.CancelIcon(), + OnTapped: d.Hide, + } + ok := &widget.Button{ + Text: confirm, Icon: theme.ConfirmIcon(), Importance: widget.HighImportance, + OnTapped: func() { + d.hideWithResponse(true) + }, + } + d.create(container.NewGridWithColumns(2, d.dismiss, ok)) + + return &ConfirmDialog{dialog: d, confirm: ok} +} + +// ShowCustomConfirm shows a dialog over the specified application using custom +// content. The cancel button will have the dismiss text set and the "OK" will use +// the confirm text. The response callback is called on user action. +// The MinSize() of the CanvasObject passed will be used to set the size of the window. +func ShowCustomConfirm(title, confirm, dismiss string, content fyne.CanvasObject, + callback func(bool), parent fyne.Window, +) { + NewCustomConfirm(title, confirm, dismiss, content, callback, parent).Show() +} diff --git a/vendor/fyne.io/fyne/v2/dialog/entry.go b/vendor/fyne.io/fyne/v2/dialog/entry.go new file mode 100644 index 0000000..3f5c542 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/dialog/entry.go @@ -0,0 +1,75 @@ +package dialog + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/lang" + "fyne.io/fyne/v2/widget" +) + +// EntryDialog is a variation of a dialog which prompts the user to enter some text. +// +// Deprecated: Use dialog.NewForm() or dialog.ShowForm() with a widget.Entry inside instead. +type EntryDialog struct { + *FormDialog + + entry *widget.Entry + + onClosed func() +} + +// SetText changes the current text value of the entry dialog, this can +// be useful for setting a default value. +func (i *EntryDialog) SetText(s string) { + i.entry.SetText(s) +} + +// SetPlaceholder defines the placeholder text for the entry +func (i *EntryDialog) SetPlaceholder(s string) { + i.entry.SetPlaceHolder(s) +} + +// SetOnClosed changes the callback which is run when the dialog is closed, +// which is nil by default. +// +// The callback is called unconditionally whether the user confirms or cancels. +// +// Note that the callback will be called after onConfirm, if both are non-nil. +// This way onConfirm can potential modify state that this callback needs to +// get the user input when the user confirms, while also being able to handle +// the case where the user cancelled. +func (i *EntryDialog) SetOnClosed(callback func()) { + i.onClosed = callback +} + +// NewEntryDialog creates a dialog over the specified window for the user to enter a value. +// +// onConfirm is a callback that runs when the user enters a string of +// text and clicks the "confirm" button. May be nil. +// +// Deprecated: Use dialog.NewForm() with a widget.Entry inside instead. +func NewEntryDialog(title, message string, onConfirm func(string), parent fyne.Window) *EntryDialog { + i := &EntryDialog{entry: widget.NewEntry()} + items := []*widget.FormItem{widget.NewFormItem(message, i.entry)} + i.FormDialog = NewForm(title, lang.L("OK"), lang.L("Cancel"), items, func(ok bool) { + // User has confirmed and entered an input + if ok && onConfirm != nil { + onConfirm(i.entry.Text) + } + + if i.onClosed != nil { + i.onClosed() + } + + i.entry.Text = "" + i.win.Hide() // Close directly without executing the callback. This is the callback. + }, parent) + + return i +} + +// ShowEntryDialog creates a new entry dialog and shows it immediately. +// +// Deprecated: Use dialog.ShowForm() with a widget.Entry inside instead. +func ShowEntryDialog(title, message string, onConfirm func(string), parent fyne.Window) { + NewEntryDialog(title, message, onConfirm, parent).Show() +} diff --git a/vendor/fyne.io/fyne/v2/dialog/file.go b/vendor/fyne.io/fyne/v2/dialog/file.go new file mode 100644 index 0000000..d1cbef2 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/dialog/file.go @@ -0,0 +1,991 @@ +package dialog + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "runtime" + "sort" + "strings" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/lang" + "fyne.io/fyne/v2/storage" + "fyne.io/fyne/v2/storage/repository" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" +) + +// ViewLayout can be passed to SetView() to set the view of +// a FileDialog +// +// Since: 2.5 +type ViewLayout int + +const ( + defaultView ViewLayout = iota + ListView + GridView +) + +const ( + viewLayoutKey = "fyne:fileDialogViewLayout" + lastFolderKey = "fyne:fileDialogLastFolder" +) + +type textWidget interface { + fyne.Widget + SetText(string) +} + +type favoriteItem struct { + locName string + locIcon fyne.Resource + loc fyne.URI +} + +type fileDialogPanel interface { + fyne.Widget + + Unselect(int) +} + +type fileDialog struct { + file *FileDialog + fileName textWidget + title *widget.Label + dismiss *widget.Button + open *widget.Button + breadcrumb *fyne.Container + breadcrumbScroll *container.Scroll + files fileDialogPanel + filesScroll *container.Scroll + favorites []favoriteItem + favoritesList *widget.List + showHidden bool + + view ViewLayout + + data []fyne.URI + + win *widget.PopUp + selected fyne.URI + selectedID int + dir fyne.ListableURI + // this will be the initial filename in a FileDialog in save mode + initialFileName string + + toggleViewButton *widget.Button +} + +// FileDialog is a dialog containing a file picker for use in opening or saving files. +type FileDialog struct { + callback any + onClosedCallback func(bool) + parent fyne.Window + dialog *fileDialog + + titleText string + confirmText, dismissText string + desiredSize fyne.Size + filter storage.FileFilter + save bool + // this will be applied to dialog.dir when it's loaded + startingLocation fyne.ListableURI + // this will be the initial filename in a FileDialog in save mode + initialFileName string + // this will be the initial view in a FileDialog + initialView ViewLayout +} + +// Declare conformity to Dialog interface +var _ Dialog = (*FileDialog)(nil) + +func (f *fileDialog) makeUI() fyne.CanvasObject { + if f.file.save { + saveName := widget.NewEntry() + saveName.OnChanged = func(s string) { + if s == "" { + f.open.Disable() + } else { + f.open.Enable() + } + } + saveName.SetPlaceHolder(lang.L("Enter filename")) + saveName.OnSubmitted = func(s string) { + f.open.OnTapped() + } + f.fileName = saveName + } else { + f.fileName = widget.NewLabel("") + } + + label := lang.L("Open") + if f.file.save { + label = lang.L("Save") + } + if f.file.confirmText != "" { + label = f.file.confirmText + } + f.open = f.makeOpenButton(label) + + if f.file.save { + f.fileName.SetText(f.initialFileName) + } + + dismissLabel := lang.L("Cancel") + if f.file.dismissText != "" { + dismissLabel = f.file.dismissText + } + f.dismiss = f.makeDismissButton(dismissLabel) + + buttons := container.NewGridWithRows(1, f.dismiss, f.open) + + f.filesScroll = container.NewScroll(nil) // filesScroll's content will be set by setView function. + verticalExtra := float32(float64(fileIconSize) * 0.25) + itemMin := f.newFileItem(storage.NewFileURI("filename.txt"), false, false).MinSize() + f.filesScroll.SetMinSize(itemMin.AddWidthHeight(itemMin.Width+theme.Padding()*3, verticalExtra)) + + f.breadcrumb = container.NewHBox() + f.breadcrumbScroll = container.NewHScroll(container.NewPadded(f.breadcrumb)) + title := label + " " + lang.L("File") + if f.file.isDirectory() { + title = label + " " + lang.L("Folder") + } + if f.file.titleText != "" { + title = f.file.titleText + } + f.title = widget.NewLabelWithStyle(title, fyne.TextAlignLeading, fyne.TextStyle{Bold: true}) + + view := ViewLayout(fyne.CurrentApp().Preferences().Int(viewLayoutKey)) + + // handle invalid values + if view != GridView && view != ListView { + view = defaultView + } + + if view == defaultView { + // set GridView as default + view = GridView + + if f.file.initialView != defaultView { + view = f.file.initialView + } + } + + // icon of button is set in subsequent setView() call + f.toggleViewButton = widget.NewButtonWithIcon("", nil, func() { + if f.view == GridView { + f.setView(ListView) + } else { + f.setView(GridView) + } + }) + f.setView(view) + + f.loadFavorites() + + f.favoritesList = widget.NewList( + func() int { + return len(f.favorites) + }, + func() fyne.CanvasObject { + return container.NewHBox(container.New(&iconPaddingLayout{}, widget.NewIcon(theme.DocumentIcon())), widget.NewLabel("Template Object")) + }, + func(id widget.ListItemID, item fyne.CanvasObject) { + item.(*fyne.Container).Objects[0].(*fyne.Container).Objects[0].(*widget.Icon).SetResource(f.favorites[id].locIcon) + item.(*fyne.Container).Objects[1].(*widget.Label).SetText(f.favorites[id].locName) + }, + ) + f.favoritesList.OnSelected = func(id widget.ListItemID) { + f.setLocation(f.favorites[id].loc) + } + + var optionsButton *widget.Button + optionsButton = widget.NewButtonWithIcon("", theme.SettingsIcon(), func() { + f.optionsMenu(fyne.CurrentApp().Driver().AbsolutePositionForObject(optionsButton), optionsButton.Size()) + }) + + newFolderButton := widget.NewButtonWithIcon("", theme.FolderNewIcon(), func() { + newFolderEntry := widget.NewEntry() + ShowForm(lang.L("New Folder"), lang.L("Create Folder"), lang.L("Cancel"), []*widget.FormItem{ + { + Text: lang.X("file.name", "Name"), + Widget: newFolderEntry, + }, + }, func(s bool) { + if !s || newFolderEntry.Text == "" { + return + } + + newFolderPath := filepath.Join(f.dir.Path(), newFolderEntry.Text) + createFolderErr := os.MkdirAll(newFolderPath, 0o750) + if createFolderErr != nil { + fyne.LogError( + fmt.Sprintf("Failed to create folder with path %s", newFolderPath), + createFolderErr, + ) + ShowError(errors.New("folder cannot be created"), f.file.parent) + } + f.refreshDir(f.dir) + }, f.file.parent) + }) + + optionsbuttons := container.NewHBox( + newFolderButton, + f.toggleViewButton, + optionsButton, + ) + + header := container.NewBorder(nil, nil, nil, optionsbuttons, + f.title, + ) + + footer := container.NewBorder(nil, nil, nil, buttons, + container.NewHScroll(f.fileName), + ) + + body := container.NewHSplit( + f.favoritesList, + container.NewBorder(f.breadcrumbScroll, nil, nil, nil, + f.filesScroll, + ), + ) + body.SetOffset(0) // Set the minimum offset so that the favoritesList takes only its minimal width + + return container.NewBorder(header, footer, nil, nil, body) +} + +func (f *fileDialog) makeOpenButton(label string) *widget.Button { + btn := widget.NewButton(label, func() { + if f.file.callback == nil { + f.win.Hide() + if f.file.onClosedCallback != nil { + f.file.onClosedCallback(false) + } + return + } + + if f.file.save { + callback := f.file.callback.(func(fyne.URIWriteCloser, error)) + name := f.fileName.(*widget.Entry).Text + location, _ := storage.Child(f.dir, name) + + exists, _ := storage.Exists(location) + if !exists { + f.win.Hide() + if f.file.onClosedCallback != nil { + f.file.onClosedCallback(true) + } + callback(storage.Writer(location)) + return + } + + listable, err := storage.CanList(location) + if err == nil && listable { + ShowInformation("Cannot overwrite", + "Files cannot replace a directory,\ncheck the file name and try again", f.file.parent) + return + } + + ShowConfirm("Overwrite?", "Are you sure you want to overwrite the file\n"+name+"?", + func(ok bool) { + if !ok { + return + } + f.win.Hide() + + callback(storage.Writer(location)) + if f.file.onClosedCallback != nil { + f.file.onClosedCallback(true) + } + }, f.file.parent) + } else if f.selected != nil { + callback := f.file.callback.(func(fyne.URIReadCloser, error)) + f.win.Hide() + if f.file.onClosedCallback != nil { + f.file.onClosedCallback(true) + } + callback(storage.Reader(f.selected)) + } else if f.file.isDirectory() { + callback := f.file.callback.(func(fyne.ListableURI, error)) + f.win.Hide() + if f.file.onClosedCallback != nil { + f.file.onClosedCallback(true) + } + callback(f.dir, nil) + } + }) + + btn.Importance = widget.HighImportance + btn.Disable() + + return btn +} + +func (f *fileDialog) makeDismissButton(label string) *widget.Button { + btn := widget.NewButton(label, func() { + f.win.Hide() + if f.file.onClosedCallback != nil { + f.file.onClosedCallback(false) + } + if f.file.callback != nil { + if f.file.save { + f.file.callback.(func(fyne.URIWriteCloser, error))(nil, nil) + } else if f.file.isDirectory() { + f.file.callback.(func(fyne.ListableURI, error))(nil, nil) + } else { + f.file.callback.(func(fyne.URIReadCloser, error))(nil, nil) + } + } + }) + + return btn +} + +func (f *fileDialog) optionsMenu(position fyne.Position, buttonSize fyne.Size) { + hiddenFiles := widget.NewCheck(lang.L("Show Hidden Files"), func(changed bool) { + f.showHidden = changed + f.refreshDir(f.dir) + }) + hiddenFiles.Checked = f.showHidden + hiddenFiles.Refresh() + content := container.NewVBox(hiddenFiles) + + p := position.Add(buttonSize) + pos := fyne.NewPos(p.X-content.MinSize().Width-theme.Padding()*2, p.Y+theme.Padding()*2) + widget.ShowPopUpAtPosition(content, f.win.Canvas, pos) +} + +func getFavoriteLocations() (map[string]fyne.ListableURI, error) { + if runtime.GOOS == "js" { + return make(map[string]fyne.ListableURI), nil + } + + homeDir, err := os.UserHomeDir() + if err != nil { + return nil, err + } + homeURI := storage.NewFileURI(homeDir) + home, _ := storage.ListerForURI(homeURI) + + favoriteLocations := map[string]fyne.ListableURI{"Home": home} + for _, favName := range getFavoritesOrder() { + uri, err1 := getFavoriteLocation(homeURI, favName) + if err != nil { + err = err1 + continue + } + + listURI, err1 := storage.ListerForURI(uri) + if err1 != nil { + err = err1 + continue + } + favoriteLocations[favName] = listURI + } + + return favoriteLocations, err +} + +func (f *fileDialog) loadFavorites() { + favoriteLocations, err := getFavoriteLocations() + if err != nil { + fyne.LogError("Getting favorite locations", err) + } + + f.favorites = []favoriteItem{ + {locName: "Home", locIcon: theme.HomeIcon(), loc: favoriteLocations["Home"]}, + } + app := fyne.CurrentApp() + if hasAppFiles(app) { + f.favorites = append(f.favorites, + favoriteItem{locName: "App Files", locIcon: theme.FileIcon(), loc: storageURI(app)}) + } + f.favorites = append(f.favorites, f.getPlaces()...) + + for _, locName := range getFavoritesOrder() { + loc, ok := favoriteLocations[locName] + if !ok { + continue + } + locIcon := getFavoritesIcon(locName) + f.favorites = append(f.favorites, + favoriteItem{locName: locName, locIcon: locIcon, loc: loc}) + } +} + +func (f *fileDialog) refreshDir(dir fyne.ListableURI) { + f.data = nil + + files, err := dir.List() + if err != nil { + fyne.LogError("Unable to read ListableURI "+dir.String(), err) + return + } + + var icons []fyne.URI + parent, err := storage.Parent(dir) + if err != nil && err != repository.ErrURIRoot { + fyne.LogError("Unable to get parent of "+dir.String(), err) + return + } + if parent != nil && parent.String() != dir.String() { + icons = append(icons, parent) + } + + for _, file := range files { + if !f.showHidden && isHidden(file) { + continue + } + + listable, err := storage.ListerForURI(file) + if f.file.isDirectory() && err != nil { + continue + } else if err == nil { // URI points to a directory + icons = append(icons, listable) + } else if f.file.filter == nil || f.file.filter.Matches(file) { + icons = append(icons, file) + } + } + + toSort := icons + if parent != nil { + toSort = icons[1:] + } + sort.Slice(toSort, func(i, j int) bool { + if parent != nil { // avoiding the parent in [0] + i++ + j++ + } + + return strings.ToLower(icons[i].Name()) < strings.ToLower(icons[j].Name()) + }) + f.data = icons + + f.files.Refresh() + f.filesScroll.Offset = fyne.NewPos(0, 0) + f.filesScroll.Refresh() +} + +func (f *fileDialog) setLocation(dir fyne.URI) error { + if dir == nil { + return errors.New("failed to open nil directory") + } + + if f.selectedID > -1 { + f.files.Unselect(f.selectedID) + } + + list, err := storage.ListerForURI(dir) + if err != nil { + return err + } + + fyne.CurrentApp().Preferences().SetString(lastFolderKey, dir.String()) + isFav := false + for i, fav := range f.favorites { + if storage.EqualURI(fav.loc, dir) { + f.favoritesList.Select(i) + isFav = true + break + } + } + if !isFav { + f.favoritesList.UnselectAll() + } + + f.setSelected(nil, -1) + f.dir = list + + f.breadcrumb.Objects = nil + for parent := dir; parent != nil && err == nil; parent, err = storage.Parent(parent) { + currentParent := parent + f.breadcrumb.Add( + widget.NewButton(currentParent.Name(), func() { + err := f.setLocation(currentParent) + if err != nil { + fyne.LogError("Failed to set directory", err) + } + }), + ) + } + + // Use slices.Reverse with Go 1.21: + objects := f.breadcrumb.Objects + for i, j := 0, len(objects)-1; i < j; i, j = i+1, j-1 { + objects[i], objects[j] = objects[j], objects[i] + } + + f.breadcrumbScroll.Refresh() + f.breadcrumbScroll.Offset.X = f.breadcrumbScroll.Content.Size().Width - f.breadcrumbScroll.Size().Width + f.breadcrumbScroll.Refresh() + + if f.file.isDirectory() { + f.fileName.SetText(dir.Name()) + f.open.Enable() + } + f.refreshDir(list) + + return nil +} + +func (f *fileDialog) setSelected(file fyne.URI, id int) { + if file != nil { + if listable, err := storage.CanList(file); err == nil && listable { + f.setLocation(file) + return + } + } + f.selected = file + f.selectedID = id + + if file == nil || file.Path() == "" { + // keep user input while navigating + // in a FileSave dialog + if !f.file.save { + f.fileName.SetText("") + f.open.Disable() + } + } else { + f.fileName.SetText(file.Name()) + f.open.Enable() + } +} + +func (f *fileDialog) setView(view ViewLayout) { + f.view = view + fyne.CurrentApp().Preferences().SetInt(viewLayoutKey, int(view)) + var selectF func(id int) + choose := func(id int) { + if file, ok := f.getDataItem(id); ok { + f.selectedID = id + f.setSelected(file, id) + } + } + count := func() int { + return len(f.data) + } + template := func() fyne.CanvasObject { + return f.newFileItem(storage.NewFileURI("./tempfile"), true, false) + } + update := func(id widget.GridWrapItemID, o fyne.CanvasObject) { + if dir, ok := f.getDataItem(id); ok { + parent := id == 0 && len(dir.Path()) < len(f.dir.Path()) + _, isDir := dir.(fyne.ListableURI) + o.(*fileDialogItem).setLocation(dir, isDir || parent, parent) + o.(*fileDialogItem).choose = selectF + o.(*fileDialogItem).id = id + o.(*fileDialogItem).open = f.open.OnTapped + } + } + // Actually, during the real interaction, the OnSelected won't be called. + // It will be called only when we directly calls container.select(i) + if f.view == GridView { + grid := widget.NewGridWrap(count, template, update) + grid.OnSelected = choose + f.files = grid + f.toggleViewButton.SetIcon(theme.ListIcon()) + selectF = grid.Select + } else { + list := widget.NewList(count, template, update) + list.OnSelected = choose + f.files = list + f.toggleViewButton.SetIcon(theme.GridIcon()) + selectF = list.Select + } + + if f.dir != nil { + f.refreshDir(f.dir) + } + f.filesScroll.Content = container.NewPadded(f.files) + f.filesScroll.Refresh() +} + +func (f *fileDialog) getDataItem(id int) (fyne.URI, bool) { + if id >= len(f.data) { + return nil, false + } + + return f.data[id], true +} + +// effectiveStartingDir calculates the directory at which the file dialog should +// open, based on the values of startingDirectory, CWD, home, and any error +// conditions which occur. +// +// Order of precedence is: +// +// - file.startingDirectory if non-empty, os.Stat()-able, and uses the file:// +// URI scheme +// - previously used file open/close folder within this app +// - the current app's document storage, if App.Storage() documents have been saved +// - os.UserHomeDir() +// - os.Getwd() +// - "/" (should be filesystem root on all supported platforms) +func (f *FileDialog) effectiveStartingDir() fyne.ListableURI { + if f.startingLocation != nil { + if f.startingLocation.Scheme() == "file" { + path := f.startingLocation.Path() + + // the starting directory is set explicitly + if _, err := os.Stat(path); err != nil { + fyne.LogError("Error with StartingLocation", err) + } else { + return f.startingLocation + } + } + return f.startingLocation + } + + // last used + lastPath := fyne.CurrentApp().Preferences().String(lastFolderKey) + if lastPath != "" { + parsed, err := storage.ParseURI(lastPath) + if err == nil { + dir, err := storage.ListerForURI(parsed) + if err == nil { + return dir + } + } + } + + // Try app storage + app := fyne.CurrentApp() + if hasAppFiles(app) { + list, _ := storage.ListerForURI(storageURI(app)) + return list + } + + // Try home dir + dir, err := os.UserHomeDir() + if err == nil { + lister, err := storage.ListerForURI(storage.NewFileURI(dir)) + if err == nil { + return lister + } + fyne.LogError("Could not create lister for user home dir", err) + } + fyne.LogError("Could not load user home dir", err) + + // Try to get ./ + wd, err := os.Getwd() + if err == nil { + lister, err := storage.ListerForURI(storage.NewFileURI(wd)) + if err == nil { + return lister + } + fyne.LogError("Could not create lister for working dir", err) + } + + lister, err := storage.ListerForURI(storage.NewFileURI("/")) + if err != nil { + fyne.LogError("could not create lister for /", err) + return nil + } + return lister +} + +func showFile(file *FileDialog) *fileDialog { + d := &fileDialog{file: file, initialFileName: file.initialFileName, view: GridView} + ui := d.makeUI() + pad := theme.Padding() + itemMin := d.newFileItem(storage.NewFileURI("filename.txt"), false, false).MinSize() + size := ui.MinSize().Add(itemMin.AddWidthHeight(itemMin.Width+pad*4, pad*2)) + + d.win = widget.NewModalPopUp(ui, file.parent.Canvas()) + d.win.Resize(size) + + d.setLocation(file.effectiveStartingDir()) + d.win.Show() + if file.save { + d.win.Canvas.Focus(d.fileName.(*widget.Entry)) + } + return d +} + +// Dismiss instructs the dialog to close without any affirmative action. +// +// Since: 2.6 +func (f *FileDialog) Dismiss() { + f.dialog.dismiss.OnTapped() +} + +// MinSize returns the size that this dialog should not shrink below +// +// Since: 2.1 +func (f *FileDialog) MinSize() fyne.Size { + return f.dialog.win.MinSize() +} + +// Show shows the file dialog. +func (f *FileDialog) Show() { + if f.save { + if fileSaveOSOverride(f) { + return + } + } else { + if fileOpenOSOverride(f) { + return + } + } + if f.dialog != nil { + f.dialog.win.Show() + return + } + f.dialog = showFile(f) + if !f.desiredSize.IsZero() { + f.Resize(f.desiredSize) + } +} + +// Refresh causes this dialog to be updated +func (f *FileDialog) Refresh() { + f.dialog.win.Refresh() +} + +// Resize dialog to the requested size, if there is sufficient space. +// If the parent window is not large enough then the size will be reduced to fit. +func (f *FileDialog) Resize(size fyne.Size) { + f.desiredSize = size + if f.dialog == nil { + return + } + f.dialog.win.Resize(size) +} + +// Hide hides the file dialog. +func (f *FileDialog) Hide() { + if f.dialog == nil { + return + } + f.dialog.win.Hide() + if f.onClosedCallback != nil { + f.onClosedCallback(false) + } +} + +// SetConfirmText allows custom text to be set in the confirmation button +// +// Since: 2.2 +func (f *FileDialog) SetConfirmText(label string) { + f.confirmText = label + if f.dialog == nil { + return + } + f.dialog.open.SetText(label) + f.dialog.win.Refresh() +} + +// SetDismissText allows custom text to be set in the dismiss button +func (f *FileDialog) SetDismissText(label string) { + f.dismissText = label + if f.dialog == nil { + return + } + f.dialog.dismiss.SetText(label) + f.dialog.win.Refresh() +} + +// SetTitleText allows custom text to be set in the dialog title +// +// Since: 2.6 +func (f *FileDialog) SetTitleText(label string) { + f.titleText = label + if f.dialog == nil { + return + } + f.dialog.title.SetText(label) +} + +// SetLocation tells this FileDialog which location to display. +// This is normally called before the dialog is shown. +// +// Since: 1.4 +func (f *FileDialog) SetLocation(u fyne.ListableURI) { + f.startingLocation = u + if f.dialog != nil { + f.dialog.setLocation(u) + } +} + +// SetOnClosed sets a callback function that is called when +// the dialog is closed. +func (f *FileDialog) SetOnClosed(closed func()) { + // If there is already a callback set, remember it and call both. + originalCallback := f.onClosedCallback + + f.onClosedCallback = func(response bool) { + if f.dialog == nil { + return + } + if originalCallback != nil { + originalCallback(response) + } + closed() + } +} + +// SetFilter sets a filter for limiting files that can be chosen in the file dialog. +func (f *FileDialog) SetFilter(filter storage.FileFilter) { + if f.isDirectory() { + fyne.LogError("Cannot set a filter for a folder dialog", nil) + return + } + f.filter = filter + if f.dialog != nil { + f.dialog.refreshDir(f.dialog.dir) + } +} + +// SetFileName sets the filename in a FileDialog in save mode. +// This is normally called before the dialog is shown. +func (f *FileDialog) SetFileName(fileName string) { + if f.save { + f.initialFileName = fileName + // Update entry if fileDialog has already been created + if f.dialog != nil { + f.dialog.fileName.SetText(fileName) + } + } +} + +// SetView changes the default display view of the FileDialog +// This is normally called before the dialog is shown. +// +// Since: 2.5 +func (f *FileDialog) SetView(v ViewLayout) { + f.initialView = v + if f.dialog != nil { + f.dialog.setView(v) + } +} + +// NewFileOpen creates a file dialog allowing the user to choose a file to open. +// +// The callback function will run when the dialog closes and provide a reader for the chosen file. +// The reader will be nil when the user cancels or when nothing is selected. +// When the reader isn't nil it must be closed by the callback. +// +// The dialog will appear over the window specified when Show() is called. +func NewFileOpen(callback func(reader fyne.URIReadCloser, err error), parent fyne.Window) *FileDialog { + dialog := &FileDialog{callback: callback, parent: parent} + return dialog +} + +// NewFileSave creates a file dialog allowing the user to choose a file to save +// to (new or overwrite). If the user chooses an existing file they will be +// asked if they are sure. +// +// The callback function will run when the dialog closes and provide a writer for the chosen file. +// The writer will be nil when the user cancels or when nothing is selected. +// When the writer isn't nil it must be closed by the callback. +// +// The dialog will appear over the window specified when Show() is called. +func NewFileSave(callback func(writer fyne.URIWriteCloser, err error), parent fyne.Window) *FileDialog { + dialog := &FileDialog{callback: callback, parent: parent, save: true} + return dialog +} + +// ShowFileOpen creates and shows a file dialog allowing the user to choose a +// file to open. +// +// The callback function will run when the dialog closes and provide a reader for the chosen file. +// The reader will be nil when the user cancels or when nothing is selected. +// When the reader isn't nil it must be closed by the callback. +// +// The dialog will appear over the window specified. +func ShowFileOpen(callback func(reader fyne.URIReadCloser, err error), parent fyne.Window) { + dialog := NewFileOpen(callback, parent) + if fileOpenOSOverride(dialog) { + return + } + dialog.Show() +} + +// ShowFileSave creates and shows a file dialog allowing the user to choose a +// file to save to (new or overwrite). If the user chooses an existing file they +// will be asked if they are sure. +// +// The callback function will run when the dialog closes and provide a writer for the chosen file. +// The writer will be nil when the user cancels or when nothing is selected. +// When the writer isn't nil it must be closed by the callback. +// +// The dialog will appear over the window specified. +func ShowFileSave(callback func(writer fyne.URIWriteCloser, err error), parent fyne.Window) { + dialog := NewFileSave(callback, parent) + if fileSaveOSOverride(dialog) { + return + } + dialog.Show() +} + +func getFavoritesIcon(location string) fyne.Resource { + switch location { + case "Documents": + return theme.DocumentIcon() + case "Desktop": + return theme.DesktopIcon() + case "Downloads": + return theme.DownloadIcon() + case "Music": + return theme.MediaMusicIcon() + case "Pictures": + return theme.MediaPhotoIcon() + case "Videos": + return theme.MediaVideoIcon() + } + + if (runtime.GOOS == "darwin" && location == "Movies") || + (runtime.GOOS != "darwin" && location == "Videos") { + return theme.MediaVideoIcon() + } + + return nil +} + +func getFavoritesOrder() [6]string { + order := [6]string{ + "Desktop", + "Documents", + "Downloads", + "Music", + "Pictures", + "Videos", + } + + if runtime.GOOS == "darwin" { + order[5] = "Movies" + } + + return order +} + +func hasAppFiles(a fyne.App) bool { + if a.UniqueID() == "testApp" { + return false + } + + return len(a.Storage().List()) > 0 +} + +func storageURI(a fyne.App) fyne.URI { + dir, _ := storage.Child(a.Storage().RootURI(), "Documents") + return dir +} + +// iconPaddingLayout adds padding to the left of a widget.Icon(). +// NOTE: It assumes that the slice only contains one item. +type iconPaddingLayout struct{} + +func (i *iconPaddingLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) { + padding := theme.Padding() * 2 + objects[0].Move(fyne.NewPos(padding, 0)) + objects[0].Resize(size.SubtractWidthHeight(padding, 0)) +} + +func (i *iconPaddingLayout) MinSize(objects []fyne.CanvasObject) fyne.Size { + return objects[0].MinSize().AddWidthHeight(theme.Padding()*2, 0) +} diff --git a/vendor/fyne.io/fyne/v2/dialog/file_darwin.go b/vendor/fyne.io/fyne/v2/dialog/file_darwin.go new file mode 100644 index 0000000..7bab6ed --- /dev/null +++ b/vendor/fyne.io/fyne/v2/dialog/file_darwin.go @@ -0,0 +1,12 @@ +//go:build !ios && !android && !wasm && !js + +package dialog + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/storage" +) + +func getFavoriteLocation(homeURI fyne.URI, name string) (fyne.URI, error) { + return storage.Child(homeURI, name) +} diff --git a/vendor/fyne.io/fyne/v2/dialog/file_mobile.go b/vendor/fyne.io/fyne/v2/dialog/file_mobile.go new file mode 100644 index 0000000..e05cf05 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/dialog/file_mobile.go @@ -0,0 +1,43 @@ +//go:build ios || android + +package dialog + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/driver/mobile" + "fyne.io/fyne/v2/storage" +) + +func (f *fileDialog) getPlaces() []favoriteItem { + return []favoriteItem{} +} + +func isHidden(file fyne.URI) bool { + if file.Scheme() != "file" { + fyne.LogError("Cannot check if non file is hidden", nil) + return false + } + return false +} + +func hideFile(filename string) error { + return nil +} + +func fileOpenOSOverride(f *FileDialog) bool { + if f.isDirectory() { + mobile.ShowFolderOpenPicker(f.callback.(func(fyne.ListableURI, error))) + } else { + mobile.ShowFileOpenPicker(f.callback.(func(fyne.URIReadCloser, error)), f.filter) + } + return true +} + +func fileSaveOSOverride(f *FileDialog) bool { + mobile.ShowFileSavePicker(f.callback.(func(fyne.URIWriteCloser, error)), f.filter, f.initialFileName) + return true +} + +func getFavoriteLocation(homeURI fyne.URI, name string) (fyne.URI, error) { + return storage.Child(homeURI, name) +} diff --git a/vendor/fyne.io/fyne/v2/dialog/file_tamago.go b/vendor/fyne.io/fyne/v2/dialog/file_tamago.go new file mode 100644 index 0000000..786080e --- /dev/null +++ b/vendor/fyne.io/fyne/v2/dialog/file_tamago.go @@ -0,0 +1,9 @@ +//go:build tamago || noos + +package dialog + +import "fyne.io/fyne/v2" + +func getFavoriteLocations() (map[string]fyne.ListableURI, error) { + return map[string]fyne.ListableURI{}, nil +} diff --git a/vendor/fyne.io/fyne/v2/dialog/file_unix.go b/vendor/fyne.io/fyne/v2/dialog/file_unix.go new file mode 100644 index 0000000..d32bc50 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/dialog/file_unix.go @@ -0,0 +1,38 @@ +//go:build !windows && !android && !ios && !wasm && !js + +package dialog + +import ( + "path/filepath" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/storage" + "fyne.io/fyne/v2/theme" +) + +func (f *fileDialog) getPlaces() []favoriteItem { + lister, err := storage.ListerForURI(storage.NewFileURI("/")) + if err != nil { + fyne.LogError("could not create lister for /", err) + return []favoriteItem{} + } + return []favoriteItem{{ + "Computer", + theme.ComputerIcon(), + lister, + }} +} + +func isHidden(file fyne.URI) bool { + if file.Scheme() != "file" { + fyne.LogError("Cannot check if non file is hidden", nil) + return false + } + + name := filepath.Base(file.Path()) + return name == "" || name[0] == '.' +} + +func hideFile(_ string) error { + return nil +} diff --git a/vendor/fyne.io/fyne/v2/dialog/file_wasm.go b/vendor/fyne.io/fyne/v2/dialog/file_wasm.go new file mode 100644 index 0000000..bbd55dd --- /dev/null +++ b/vendor/fyne.io/fyne/v2/dialog/file_wasm.go @@ -0,0 +1,33 @@ +//go:build wasm || js + +package dialog + +import ( + "fyne.io/fyne/v2" +) + +func (f *fileDialog) loadPlaces() []fyne.CanvasObject { + return nil +} + +func isHidden(file fyne.URI) bool { + return false +} + +func fileOpenOSOverride(f *FileDialog) bool { + // TODO #2737 + return true +} + +func fileSaveOSOverride(f *FileDialog) bool { + // TODO #2738 + return true +} + +func (f *fileDialog) getPlaces() []favoriteItem { + return []favoriteItem{} +} + +func getFavoriteLocation(homeURI fyne.URI, name string) (fyne.URI, error) { + return nil, nil +} diff --git a/vendor/fyne.io/fyne/v2/dialog/file_windows.go b/vendor/fyne.io/fyne/v2/dialog/file_windows.go new file mode 100644 index 0000000..8f46ed7 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/dialog/file_windows.go @@ -0,0 +1,102 @@ +package dialog + +import ( + "os" + "syscall" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/storage" + "fyne.io/fyne/v2/theme" +) + +func driveMask() uint32 { + dll, err := syscall.LoadLibrary("kernel32.dll") + if err != nil { + fyne.LogError("Error loading kernel32.dll", err) + return 0 + } + handle, err := syscall.GetProcAddress(dll, "GetLogicalDrives") + if err != nil { + fyne.LogError("Could not find GetLogicalDrives call", err) + return 0 + } + + ret, _, err := syscall.SyscallN(uintptr(handle)) + if err != syscall.Errno(0) { // for some reason Syscall returns something not nil on success + fyne.LogError("Error calling GetLogicalDrives", err) + return 0 + } + + return uint32(ret) +} + +func listDrives() []string { + var drives []string + mask := driveMask() + + for i := 0; i < 26; i++ { + if mask&1 == 1 { + letter := string('A' + rune(i)) + drives = append(drives, letter+":") + } + mask >>= 1 + } + + return drives +} + +func (f *fileDialog) getPlaces() []favoriteItem { + drives := listDrives() + places := make([]favoriteItem, len(drives)) + for i, drive := range drives { + driveRoot := drive + string(os.PathSeparator) // capture loop var + driveRootURI, _ := storage.ListerForURI(storage.NewFileURI(driveRoot)) + places[i] = favoriteItem{ + drive, + theme.StorageIcon(), + driveRootURI, + } + } + return places +} + +func isHidden(file fyne.URI) bool { + if file.Scheme() != "file" { + fyne.LogError("Cannot check if non file is hidden", nil) + return false + } + + point, err := syscall.UTF16PtrFromString(file.Path()) + if err != nil { + fyne.LogError("Error making string pointer", err) + return false + } + attr, err := syscall.GetFileAttributes(point) + if err != nil { + fyne.LogError("Error getting file attributes", err) + return false + } + + return attr&syscall.FILE_ATTRIBUTE_HIDDEN != 0 +} + +func hideFile(filename string) (err error) { + // git does not preserve windows hidden flag so we have to set it. + filenameW, err := syscall.UTF16PtrFromString(filename) + if err != nil { + return err + } + return syscall.SetFileAttributes(filenameW, syscall.FILE_ATTRIBUTE_HIDDEN) +} + +func fileOpenOSOverride(*FileDialog) bool { + return false +} + +func fileSaveOSOverride(*FileDialog) bool { + return false +} + +func getFavoriteLocation(homeURI fyne.URI, name string) (fyne.URI, error) { + return storage.Child(homeURI, name) +} diff --git a/vendor/fyne.io/fyne/v2/dialog/file_xdg.go b/vendor/fyne.io/fyne/v2/dialog/file_xdg.go new file mode 100644 index 0000000..403ef88 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/dialog/file_xdg.go @@ -0,0 +1,37 @@ +//go:build (linux || openbsd || freebsd || netbsd) && !android && !wasm && !js && !tamago && !noos + +package dialog + +import ( + "fmt" + "os/exec" + "strings" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/storage" +) + +func getFavoriteLocation(homeURI fyne.URI, name string) (fyne.URI, error) { + const cmdName = "xdg-user-dir" + if _, err := exec.LookPath(cmdName); err != nil { + return storage.Child(homeURI, name) // no lookup possible + } + + lookupName := strings.ToUpper(name) + cmd := exec.Command(cmdName, lookupName) + loc, err := cmd.Output() + if err != nil { + return storage.Child(homeURI, name) + } + + // Remove \n at the end + loc = loc[:len(loc)-1] + locURI := storage.NewFileURI(string(loc)) + + if strings.TrimRight(locURI.String(), "/") == strings.TrimRight(homeURI.String(), "/") { + fallback, _ := storage.Child(homeURI, name) + return fallback, fmt.Errorf("this computer does not define a %s folder", lookupName) + } + + return locURI, nil +} diff --git a/vendor/fyne.io/fyne/v2/dialog/file_xdg_flatpak.go b/vendor/fyne.io/fyne/v2/dialog/file_xdg_flatpak.go new file mode 100644 index 0000000..6f5e7ee --- /dev/null +++ b/vendor/fyne.io/fyne/v2/dialog/file_xdg_flatpak.go @@ -0,0 +1,189 @@ +//go:build flatpak && !windows && !android && !ios && !wasm && !js + +package dialog + +import ( + "strings" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/driver" + "fyne.io/fyne/v2/internal/build" + "fyne.io/fyne/v2/lang" + "fyne.io/fyne/v2/storage" + + "github.com/rymdport/portal" + "github.com/rymdport/portal/filechooser" +) + +func openFile(parentWindowHandle string, options *filechooser.OpenFileOptions) (fyne.URIReadCloser, error) { + title := lang.L("Open") + " " + lang.L("File") + uri, err := open(parentWindowHandle, title, options) + if err != nil || uri == nil { + return nil, err + } + + return storage.Reader(uri) +} + +func openFolder(parentWindowHandle string, options *filechooser.OpenFileOptions) (fyne.ListableURI, error) { + title := lang.L("Open") + " " + lang.L("Folder") + uri, err := open(parentWindowHandle, title, options) + if err != nil || uri == nil { + return nil, err + } + + return storage.ListerForURI(uri) +} + +func open(parentWindowHandle, title string, options *filechooser.OpenFileOptions) (fyne.URI, error) { + uris, err := filechooser.OpenFile(parentWindowHandle, title, options) + if err != nil { + return nil, err + } + + if len(uris) == 0 { + return nil, nil + } + + return storage.ParseURI(uris[0]) +} + +func saveFile(parentWindowHandle string, options *filechooser.SaveFileOptions) (fyne.URIWriteCloser, error) { + title := lang.L("Save") + " " + lang.L("File") + uris, err := filechooser.SaveFile(parentWindowHandle, title, options) + if err != nil { + return nil, err + } + + if len(uris) == 0 { + return nil, nil + } + + uri, err := storage.ParseURI(uris[0]) + if err != nil { + return nil, err + } + + return storage.Writer(uri) +} + +func fileOpenOSOverride(d *FileDialog) bool { + options := &filechooser.OpenFileOptions{ + Directory: d.isDirectory(), + AcceptLabel: d.confirmText, + } + if d.startingLocation != nil { + options.CurrentFolder = d.startingLocation.Path() + } + options.Filters, options.CurrentFilter = convertFilterForPortal(d.filter) + + windowHandle := windowHandleForPortal(d.parent) + + go func() { + if options.Directory { + folder, err := openFolder(windowHandle, options) + fyne.Do(func() { + folderCallback := d.callback.(func(fyne.ListableURI, error)) + folderCallback(folder, err) + }) + } else { + file, err := openFile(windowHandle, options) + fyne.Do(func() { + fileCallback := d.callback.(func(fyne.URIReadCloser, error)) + fileCallback(file, err) + }) + } + }() + + return true +} + +func fileSaveOSOverride(d *FileDialog) bool { + options := &filechooser.SaveFileOptions{ + AcceptLabel: d.confirmText, + CurrentName: d.initialFileName, + } + if d.startingLocation != nil { + options.CurrentFolder = d.startingLocation.Path() + } + options.Filters, options.CurrentFilter = convertFilterForPortal(d.filter) + + callback := d.callback.(func(fyne.URIWriteCloser, error)) + windowHandle := windowHandleForPortal(d.parent) + + go func() { + file, err := saveFile(windowHandle, options) + fyne.Do(func() { + callback(file, err) + }) + }() + + return true +} + +func windowHandleForPortal(window fyne.Window) string { + windowHandle := "" + if !build.IsWayland { + window.(driver.NativeWindow).RunNative(func(context any) { + handle := context.(driver.X11WindowContext).WindowHandle + windowHandle = portal.FormatX11WindowHandle(handle) + }) + } + + // TODO: We need to get the Wayland handle from the xdg_foreign protocol and convert to string on the form "wayland:{id}". + return windowHandle +} + +func convertFilterForPortal(fyneFilter storage.FileFilter) (list []*filechooser.Filter, current *filechooser.Filter) { + if fyneFilter == nil || fyneFilter == folderFilter { + return nil, nil + } + + if filter, ok := fyneFilter.(*storage.ExtensionFileFilter); ok { + rules := make([]filechooser.Rule, 0, 2*len(filter.Extensions)) + for _, ext := range filter.Extensions { + lowercase := filechooser.Rule{ + Type: filechooser.GlobPattern, + Pattern: "*" + strings.ToLower(ext), + } + uppercase := filechooser.Rule{ + Type: filechooser.GlobPattern, + Pattern: "*" + strings.ToUpper(ext), + } + rules = append(rules, lowercase, uppercase) + } + + name := formatFilterName(filter.Extensions, 3) + converted := &filechooser.Filter{Name: name, Rules: rules} + return []*filechooser.Filter{converted}, converted + } + + if filter, ok := fyneFilter.(*storage.MimeTypeFileFilter); ok { + rules := make([]filechooser.Rule, len(filter.MimeTypes)) + for i, mime := range filter.MimeTypes { + rules[i] = filechooser.Rule{ + Type: filechooser.MIMEType, + Pattern: mime, + } + } + + name := formatFilterName(filter.MimeTypes, 3) + converted := &filechooser.Filter{Name: name, Rules: rules} + return []*filechooser.Filter{converted}, converted + } + + return nil, nil +} + +func formatFilterName(patterns []string, count int) string { + if len(patterns) < count { + count = len(patterns) + } + + name := strings.Join(patterns[:count], ", ") + if len(patterns) > count { + name += "…" + } + + return name +} diff --git a/vendor/fyne.io/fyne/v2/dialog/file_xdg_notflatpak.go b/vendor/fyne.io/fyne/v2/dialog/file_xdg_notflatpak.go new file mode 100644 index 0000000..78528db --- /dev/null +++ b/vendor/fyne.io/fyne/v2/dialog/file_xdg_notflatpak.go @@ -0,0 +1,11 @@ +//go:build !flatpak && !windows && !android && !ios && !wasm && !js + +package dialog + +func fileOpenOSOverride(_ *FileDialog) bool { + return false +} + +func fileSaveOSOverride(_ *FileDialog) bool { + return false +} diff --git a/vendor/fyne.io/fyne/v2/dialog/fileitem.go b/vendor/fyne.io/fyne/v2/dialog/fileitem.go new file mode 100644 index 0000000..1cd9d96 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/dialog/fileitem.go @@ -0,0 +1,147 @@ +package dialog + +import ( + "path/filepath" + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/lang" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" +) + +const ( + fileIconSize = 64 + fileInlineIconSize = 24 + fileIconCellWidth = fileIconSize * 1.25 +) + +type fileDialogItem struct { + widget.BaseWidget + picker *fileDialog + + name string + id int // id in the parent container + choose func(id int) + open func() + location fyne.URI + dir bool + + lastClick time.Time +} + +func (i *fileDialogItem) CreateRenderer() fyne.WidgetRenderer { + text := widget.NewLabelWithStyle(i.name, fyne.TextAlignCenter, fyne.TextStyle{}) + text.Truncation = fyne.TextTruncateEllipsis + text.Wrapping = fyne.TextWrapBreak + icon := widget.NewFileIcon(i.location) + + return &fileItemRenderer{ + item: i, + icon: icon, + text: text, + objects: []fyne.CanvasObject{icon, text}, + fileTextSize: widget.NewLabel("M\nM").MinSize().Height, // cache two-line label height, + } +} + +func (i *fileDialogItem) setLocation(l fyne.URI, dir, up bool) { + i.dir = dir + i.location = l + i.name = l.Name() + + if i.picker.view == GridView { + ext := filepath.Ext(i.name[1:]) + i.name = i.name[:len(i.name)-len(ext)] + } + + if up { + i.name = "(" + lang.X("file.parent", "Parent") + ")" + } + + i.Refresh() +} + +func (i *fileDialogItem) Tapped(*fyne.PointEvent) { + if i.choose != nil { + i.choose(i.id) + } + now := time.Now() + if !i.dir && now.Sub(i.lastClick) < fyne.CurrentApp().Driver().DoubleTapDelay() && i.open != nil { + // It is a double click, so we ask the dialog to open + i.open() + } + i.lastClick = now +} + +func (f *fileDialog) newFileItem(location fyne.URI, dir, up bool) *fileDialogItem { + item := &fileDialogItem{ + picker: f, + location: location, + name: location.Name(), + dir: dir, + } + + if f.view == GridView { + ext := filepath.Ext(item.name[1:]) + item.name = item.name[:len(item.name)-len(ext)] + } + + if up { + item.name = "(" + lang.X("file.parent", "Parent") + ")" + } + + item.ExtendBaseWidget(item) + return item +} + +type fileItemRenderer struct { + item *fileDialogItem + fileTextSize float32 + + icon *widget.FileIcon + text *widget.Label + objects []fyne.CanvasObject +} + +func (s *fileItemRenderer) Layout(size fyne.Size) { + if s.item.picker.view == GridView { + s.icon.Resize(fyne.NewSize(fileIconSize, fileIconSize)) + s.icon.Move(fyne.NewPos((size.Width-fileIconSize)/2, 0)) + + s.text.Alignment = fyne.TextAlignCenter + s.text.Resize(fyne.NewSize(size.Width, s.fileTextSize)) + s.text.Move(fyne.NewPos(0, size.Height-s.fileTextSize)) + } else { + s.icon.Resize(fyne.NewSize(fileInlineIconSize, fileInlineIconSize)) + s.icon.Move(fyne.NewPos(theme.Padding(), (size.Height-fileInlineIconSize)/2)) + + s.text.Alignment = fyne.TextAlignLeading + textMin := s.text.MinSize() + s.text.Resize(fyne.NewSize(size.Width, textMin.Height)) + s.text.Move(fyne.NewPos(fileInlineIconSize, (size.Height-textMin.Height)/2)) + } +} + +func (s *fileItemRenderer) MinSize() fyne.Size { + if s.item.picker.view == GridView { + return fyne.NewSize(fileIconCellWidth, fileIconSize+s.fileTextSize) + } + + textMin := s.text.MinSize() + return fyne.NewSize(fileInlineIconSize+textMin.Width+theme.Padding(), textMin.Height) +} + +func (s *fileItemRenderer) Refresh() { + s.fileTextSize = widget.NewLabel("M\nM").MinSize().Height // cache two-line label height + + s.text.SetText(s.item.name) + s.icon.SetURI(s.item.location) +} + +func (s *fileItemRenderer) Objects() []fyne.CanvasObject { + return s.objects +} + +func (s *fileItemRenderer) Destroy() { +} diff --git a/vendor/fyne.io/fyne/v2/dialog/folder.go b/vendor/fyne.io/fyne/v2/dialog/folder.go new file mode 100644 index 0000000..c2d8cb2 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/dialog/folder.go @@ -0,0 +1,42 @@ +package dialog + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/storage" +) + +var folderFilter = storage.NewMimeTypeFileFilter([]string{"application/x-directory"}) + +// NewFolderOpen creates a file dialog allowing the user to choose a folder to +// open. The callback function will run when the dialog closes. The URI will be +// nil when the user cancels or when nothing is selected. +// +// The dialog will appear over the window specified when Show() is called. +// +// Since: 1.4 +func NewFolderOpen(callback func(fyne.ListableURI, error), parent fyne.Window) *FileDialog { + dialog := &FileDialog{} + dialog.callback = callback + dialog.parent = parent + dialog.filter = folderFilter + return dialog +} + +// ShowFolderOpen creates and shows a file dialog allowing the user to choose a +// folder to open. The callback function will run when the dialog closes. The +// URI will be nil when the user cancels or when nothing is selected. +// +// The dialog will appear over the window specified. +// +// Since: 1.4 +func ShowFolderOpen(callback func(fyne.ListableURI, error), parent fyne.Window) { + dialog := NewFolderOpen(callback, parent) + if fileOpenOSOverride(dialog) { + return + } + dialog.Show() +} + +func (f *FileDialog) isDirectory() bool { + return f.filter == folderFilter +} diff --git a/vendor/fyne.io/fyne/v2/dialog/form.go b/vendor/fyne.io/fyne/v2/dialog/form.go new file mode 100644 index 0000000..a595d5a --- /dev/null +++ b/vendor/fyne.io/fyne/v2/dialog/form.go @@ -0,0 +1,87 @@ +package dialog + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" +) + +// FormDialog is a simple dialog window for displaying FormItems inside a form. +// +// Since: 2.4 +type FormDialog struct { + *dialog + items []*widget.FormItem + confirm *widget.Button + cancel *widget.Button +} + +// Submit will submit the form and then hide the dialog if validation passes. +// +// Since: 2.4 +func (d *FormDialog) Submit() { + if d.confirm.Disabled() { + return + } + + d.hideWithResponse(true) +} + +// setSubmitState is intended to run when the form validation changes to +// enable/disable the submit button accordingly. +func (d *FormDialog) setSubmitState(err error) { + if err != nil { + d.confirm.Disable() + return + } + + d.confirm.Enable() +} + +// NewForm creates and returns a dialog over the specified application using +// the provided FormItems. The cancel button will have the dismiss text set and the confirm button will +// use the confirm text. The response callback is called on user action after validation passes. +// If any Validatable widget reports that validation has failed, then the confirm +// button will be disabled. The initial state of the confirm button will reflect the initial +// validation state of the items added to the form dialog. +// +// Since: 2.0 +func NewForm(title, confirm, dismiss string, items []*widget.FormItem, callback func(bool), parent fyne.Window) *FormDialog { + form := widget.NewForm(items...) + + d := &dialog{content: form, callback: callback, title: title, parent: parent} + d.dismiss = &widget.Button{ + Text: dismiss, Icon: theme.CancelIcon(), + OnTapped: d.Hide, + } + confirmBtn := &widget.Button{ + Text: confirm, Icon: theme.ConfirmIcon(), Importance: widget.HighImportance, + OnTapped: func() { d.hideWithResponse(true) }, + } + formDialog := &FormDialog{ + dialog: d, + items: items, + confirm: confirmBtn, + cancel: d.dismiss, + } + + formDialog.setSubmitState(form.Validate()) + form.SetOnValidationChanged(formDialog.setSubmitState) + + d.create(container.NewGridWithColumns(2, d.dismiss, confirmBtn)) + return formDialog +} + +// ShowForm shows a dialog over the specified application using +// the provided FormItems. The cancel button will have the dismiss text set and the confirm button will +// use the confirm text. The response callback is called on user action after validation passes. +// If any Validatable widget reports that validation has failed, then the confirm +// button will be disabled. The initial state of the confirm button will reflect the initial +// validation state of the items added to the form dialog. +// The MinSize() of the CanvasObject passed will be used to set the size of the window. +// +// Since: 2.0 +func ShowForm(title, confirm, dismiss string, content []*widget.FormItem, callback func(bool), parent fyne.Window) { + NewForm(title, confirm, dismiss, content, callback, parent).Show() +} diff --git a/vendor/fyne.io/fyne/v2/dialog/information.go b/vendor/fyne.io/fyne/v2/dialog/information.go new file mode 100644 index 0000000..6e52ca3 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/dialog/information.go @@ -0,0 +1,53 @@ +package dialog + +import ( + "unicode" + "unicode/utf8" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/lang" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" +) + +func createInformationDialog(title, message string, icon fyne.Resource, parent fyne.Window) Dialog { + d := newTextDialog(title, message, icon, parent) + d.dismiss = &widget.Button{ + Text: lang.L("OK"), + OnTapped: d.Hide, + } + d.create(container.NewGridWithColumns(1, d.dismiss)) + return d +} + +// NewInformation creates a dialog over the specified window for user information. +// The title is used for the dialog window and message is the content. +// After creation you should call Show(). +func NewInformation(title, message string, parent fyne.Window) Dialog { + return createInformationDialog(title, message, theme.InfoIcon(), parent) +} + +// ShowInformation shows a dialog over the specified window for user information. +// The title is used for the dialog window and message is the content. +func ShowInformation(title, message string, parent fyne.Window) { + NewInformation(title, message, parent).Show() +} + +// NewError creates a dialog over the specified window for an application error. +// The message is extracted from the provided error (should not be nil). +// After creation you should call Show(). +func NewError(err error, parent fyne.Window) Dialog { + dialogText := err.Error() + r, size := utf8.DecodeRuneInString(dialogText) + if r != utf8.RuneError { + dialogText = string(unicode.ToUpper(r)) + dialogText[size:] + } + return createInformationDialog(lang.L("Error"), dialogText, theme.ErrorIcon(), parent) +} + +// ShowError shows a dialog over the specified window for an application error. +// The message is extracted from the provided error (should not be nil). +func ShowError(err error, parent fyne.Window) { + NewError(err, parent).Show() +} diff --git a/vendor/fyne.io/fyne/v2/dialog/progress.go b/vendor/fyne.io/fyne/v2/dialog/progress.go new file mode 100644 index 0000000..d816c3b --- /dev/null +++ b/vendor/fyne.io/fyne/v2/dialog/progress.go @@ -0,0 +1,39 @@ +package dialog + +import ( + "image/color" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" +) + +// ProgressDialog is a simple dialog window that displays text and a progress bar. +// +// Deprecated: Use NewCustomWithoutButtons() and add a widget.ProgressBar() inside. +type ProgressDialog struct { + *dialog + + bar *widget.ProgressBar +} + +// SetValue updates the value of the progress bar - this should be between 0.0 and 1.0. +func (p *ProgressDialog) SetValue(v float64) { + p.bar.SetValue(v) +} + +// NewProgress creates a progress dialog and returns the handle. +// Using the returned type you should call Show() and then set its value through SetValue(). +// +// Deprecated: Use NewCustomWithoutButtons() and add a widget.ProgressBar() inside. +func NewProgress(title, message string, parent fyne.Window) *ProgressDialog { + d := newTextDialog(title, message, theme.InfoIcon(), parent) + bar := widget.NewProgressBar() + rect := canvas.NewRectangle(color.Transparent) + rect.SetMinSize(fyne.NewSize(200, 0)) + + d.create(container.NewStack(rect, bar)) + return &ProgressDialog{d, bar} +} diff --git a/vendor/fyne.io/fyne/v2/dialog/progressinfinite.go b/vendor/fyne.io/fyne/v2/dialog/progressinfinite.go new file mode 100644 index 0000000..e347e39 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/dialog/progressinfinite.go @@ -0,0 +1,40 @@ +package dialog + +import ( + "image/color" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" +) + +// ProgressInfiniteDialog is a simple dialog window that displays text and a infinite progress bar. +// +// Deprecated: Use NewCustomWithoutButtons() and add a widget.ProgressBarInfinite() inside. +type ProgressInfiniteDialog struct { + *dialog + + bar *widget.ProgressBarInfinite +} + +// NewProgressInfinite creates a infinite progress dialog and returns the handle. +// Using the returned type you should call Show(). +// +// Deprecated: Use NewCustomWithoutButtons() and add a widget.ProgressBarInfinite() inside. +func NewProgressInfinite(title, message string, parent fyne.Window) *ProgressInfiniteDialog { + d := newTextDialog(title, message, theme.InfoIcon(), parent) + bar := widget.NewProgressBarInfinite() + rect := canvas.NewRectangle(color.Transparent) + rect.SetMinSize(fyne.NewSize(200, 0)) + + d.create(container.NewStack(rect, bar)) + return &ProgressInfiniteDialog{d, bar} +} + +// Hide this dialog and stop the infinite progress goroutine +func (d *ProgressInfiniteDialog) Hide() { + d.bar.Hide() + d.dialog.Hide() +} diff --git a/vendor/fyne.io/fyne/v2/dialog/text.go b/vendor/fyne.io/fyne/v2/dialog/text.go new file mode 100644 index 0000000..ca2a932 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/dialog/text.go @@ -0,0 +1,50 @@ +package dialog + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" +) + +const ( + // absolute max width of text dialogs + // (prevent them from looking unnaturally large on desktop) + maxTextDialogAbsoluteWidth float32 = 600 + + // max width of text dialogs as a percentage of the current window width + maxTextDialogWinPcntWidth float32 = .9 +) + +func newTextDialog(title, message string, icon fyne.Resource, parent fyne.Window) *dialog { + d := &dialog{ + title: title, + icon: icon, + parent: parent, + content: newCenterWrappedLabel(message), + } + d.beforeShowHook = createBeforeShowHook(d, message) + + return d +} + +// returns a beforeShowHook that sets the desired width of the dialog to the min of: +// - width needed to show message without wrapping +// - maxTextDialogAbsoluteWidth +// - current window width * maxTextDialogWinPcntWidth +func createBeforeShowHook(d *dialog, message string) func() { + // Until issue #4648 is resolved, we need to create a label here + // rather than just using fyne.MeasureText, because the label's minsize + // also depends on the internal padding that label adds, which is unknown here + noWrapWidth := widget.NewLabel(message).MinSize().Width + padWidth + theme.Padding()*2 + return func() { + if d.desiredSize.IsZero() { + maxWinWitth := d.parent.Canvas().Size().Width * maxTextDialogWinPcntWidth + w := fyne.Min(fyne.Min(noWrapWidth, maxTextDialogAbsoluteWidth), maxWinWitth) + d.desiredSize = fyne.NewSize(w, d.MinSize().Height) + } + } +} + +func newCenterWrappedLabel(message string) fyne.CanvasObject { + return &widget.Label{Text: message, Alignment: fyne.TextAlignCenter, Wrapping: fyne.TextWrapWord} +} diff --git a/vendor/fyne.io/fyne/v2/driver.go b/vendor/fyne.io/fyne/v2/driver.go new file mode 100644 index 0000000..4aa7c62 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/driver.go @@ -0,0 +1,57 @@ +package fyne + +import "time" + +// Driver defines an abstract concept of a Fyne render driver. +// Any implementation must provide at least these methods. +type Driver interface { + // CreateWindow creates a new UI Window for a certain implementation. + // Developers should use [App.NewWindow]. + CreateWindow(string) Window + // AllWindows returns a slice containing all app windows. + AllWindows() []Window + + // RenderedTextSize returns the size required to render the given string of specified + // font size and style. It also returns the height to text baseline, measured from the top. + // If the source is specified it will be used, otherwise the current theme will be asked for the font. + RenderedTextSize(text string, fontSize float32, style TextStyle, source Resource) (size Size, baseline float32) + + // CanvasForObject returns the canvas that is associated with a given [CanvasObject]. + CanvasForObject(CanvasObject) Canvas + // AbsolutePositionForObject returns the position of a given [CanvasObject] relative to the top/left of a canvas. + AbsolutePositionForObject(CanvasObject) Position + + // Device returns the device that the application is currently running on. + Device() Device + // Run starts the main event loop of the driver. + Run() + // Quit closes the driver and open windows, then exit the application. + // On some operating systems this does nothing, for example iOS and Android. + Quit() + + // StartAnimation registers a new animation with this driver and requests it be started. + // Developers should use the [Animation.Start] function. + StartAnimation(*Animation) + // StopAnimation stops an animation and unregisters from this driver. + // Developers should use the [Animation.Stop] function. + StopAnimation(*Animation) + + // DoubleTapDelay returns the maximum duration where a second tap after a first one + // will be considered a [DoubleTap] instead of two distinct [Tap] events. + // + // Since: 2.5 + DoubleTapDelay() time.Duration + + // SetDisableScreenBlanking allows an app to ask the device not to sleep/lock/blank displays + // + // Since: 2.5 + SetDisableScreenBlanking(bool) + + // DoFromGoroutine provides a way to queue a function `fn` that is running on a goroutine back to + // the central thread for Fyne updates, waiting for it to return if `wait` is true. + // The driver provides the implementation normally accessed through [fyne.Do]. + // This is required when background tasks want to execute code safely in the graphical context. + // + // Since: 2.6 + DoFromGoroutine(fn func(), wait bool) +} diff --git a/vendor/fyne.io/fyne/v2/driver/desktop/app.go b/vendor/fyne.io/fyne/v2/driver/desktop/app.go new file mode 100644 index 0000000..b7346be --- /dev/null +++ b/vendor/fyne.io/fyne/v2/driver/desktop/app.go @@ -0,0 +1,23 @@ +package desktop + +import "fyne.io/fyne/v2" + +// App defines the desktop specific extensions to a fyne.App. +// +// Since: 2.2 +type App interface { + SetSystemTrayMenu(menu *fyne.Menu) + // SetSystemTrayIcon sets the icon to be used in system tray. + // If you pass a `ThemedResource` then any OS that adjusts look to match theme will adapt the icon. + SetSystemTrayIcon(icon fyne.Resource) + + // SetSystemTrayWindow optionally sets a window that this system tray will help to control. + // On systems that support it (Windows, macOS and most Linux) the window will be shown on left-tap. + // If the window is decorated (a regular window) tapping will show it only, however for a splash window + // (without window decorations) tapping when the window is visible will hide it. + // If you also have a menu set this will be triggered with right-mouse tap. + // Note that your menu should probably include a "Show Window" menu item for less-compliant Linux systems. + // + // Since: 2.7 + SetSystemTrayWindow(fyne.Window) +} diff --git a/vendor/fyne.io/fyne/v2/driver/desktop/canvas.go b/vendor/fyne.io/fyne/v2/driver/desktop/canvas.go new file mode 100644 index 0000000..0a2ab0c --- /dev/null +++ b/vendor/fyne.io/fyne/v2/driver/desktop/canvas.go @@ -0,0 +1,11 @@ +package desktop + +import "fyne.io/fyne/v2" + +// Canvas defines the desktop specific extensions to a fyne.Canvas. +type Canvas interface { + OnKeyDown() func(*fyne.KeyEvent) + SetOnKeyDown(func(*fyne.KeyEvent)) + OnKeyUp() func(*fyne.KeyEvent) + SetOnKeyUp(func(*fyne.KeyEvent)) +} diff --git a/vendor/fyne.io/fyne/v2/driver/desktop/cursor.go b/vendor/fyne.io/fyne/v2/driver/desktop/cursor.go new file mode 100644 index 0000000..f5f3b51 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/driver/desktop/cursor.go @@ -0,0 +1,47 @@ +package desktop + +import "image" + +// Cursor interface is used for objects that desire a specific cursor. +// +// Since: 2.0 +type Cursor interface { + // Image returns the image for the given cursor, or nil if none should be shown. + // It also returns the x and y pixels that should act as the hot-spot (measured from top left corner). + Image() (image.Image, int, int) +} + +// StandardCursor represents a standard Fyne cursor. +// These values were previously of type `fyne.Cursor`. +// +// Since: 2.0 +type StandardCursor int + +// Image is not used for any of the StandardCursor types. +// +// Since: 2.0 +func (d StandardCursor) Image() (image.Image, int, int) { + return nil, 0, 0 +} + +const ( + // DefaultCursor is the default cursor typically an arrow + DefaultCursor StandardCursor = iota + // TextCursor is the cursor often used to indicate text selection + TextCursor + // CrosshairCursor is the cursor often used to indicate bitmaps + CrosshairCursor + // PointerCursor is the cursor often used to indicate a link + PointerCursor + // HResizeCursor is the cursor often used to indicate horizontal resize + HResizeCursor + // VResizeCursor is the cursor often used to indicate vertical resize + VResizeCursor + // HiddenCursor will cause the cursor to not be shown + HiddenCursor +) + +// Cursorable describes any CanvasObject that needs a cursor change +type Cursorable interface { + Cursor() Cursor +} diff --git a/vendor/fyne.io/fyne/v2/driver/desktop/driver.go b/vendor/fyne.io/fyne/v2/driver/desktop/driver.go new file mode 100644 index 0000000..7502648 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/driver/desktop/driver.go @@ -0,0 +1,15 @@ +// Package desktop provides desktop specific driver functionality. +package desktop + +import "fyne.io/fyne/v2" + +// Driver represents the extended capabilities of a desktop driver +type Driver interface { + // Create a new borderless window that is centered on screen + CreateSplashWindow() fyne.Window + + // Gets the set of key modifiers that are currently active + // + // Since: 2.4 + CurrentKeyModifiers() fyne.KeyModifier +} diff --git a/vendor/fyne.io/fyne/v2/driver/desktop/key.go b/vendor/fyne.io/fyne/v2/driver/desktop/key.go new file mode 100644 index 0000000..14a544d --- /dev/null +++ b/vendor/fyne.io/fyne/v2/driver/desktop/key.go @@ -0,0 +1,66 @@ +package desktop + +import ( + "fyne.io/fyne/v2" +) + +const ( + // KeyNone represents no key + KeyNone fyne.KeyName = "" + // KeyShiftLeft represents the left shift key + KeyShiftLeft fyne.KeyName = "LeftShift" + // KeyShiftRight represents the right shift key + KeyShiftRight fyne.KeyName = "RightShift" + // KeyControlLeft represents the left control key + KeyControlLeft fyne.KeyName = "LeftControl" + // KeyControlRight represents the right control key + KeyControlRight fyne.KeyName = "RightControl" + // KeyAltLeft represents the left alt key + KeyAltLeft fyne.KeyName = "LeftAlt" + // KeyAltRight represents the right alt key + KeyAltRight fyne.KeyName = "RightAlt" + // KeySuperLeft represents the left "Windows" key (or "Command" key on macOS) + KeySuperLeft fyne.KeyName = "LeftSuper" + // KeySuperRight represents the right "Windows" key (or "Command" key on macOS) + KeySuperRight fyne.KeyName = "RightSuper" + // KeyMenu represents the left or right menu / application key + KeyMenu fyne.KeyName = "Menu" + // KeyPrintScreen represents the key used to cause a screen capture + KeyPrintScreen fyne.KeyName = "PrintScreen" + + // KeyCapsLock represents the caps lock key, tapping once is the down event then again is the up + KeyCapsLock fyne.KeyName = "CapsLock" +) + +// Modifier captures any key modifiers (shift etc.) pressed during a key event +// +// Deprecated: Use fyne.KeyModifier instead. +type Modifier = fyne.KeyModifier + +const ( + // ShiftModifier represents a shift key being held + // + // Deprecated: Use fyne.KeyModifierShift instead. + ShiftModifier = fyne.KeyModifierShift + // ControlModifier represents the ctrl key being held + // + // Deprecated: Use fyne.KeyModifierControl instead. + ControlModifier = fyne.KeyModifierControl + // AltModifier represents either alt keys being held + // + // Deprecated: Use fyne.KeyModifierAlt instead. + AltModifier = fyne.KeyModifierAlt + // SuperModifier represents either super keys being held + // + // Deprecated: Use fyne.KeyModifierSuper instead. + SuperModifier = fyne.KeyModifierSuper +) + +// Keyable describes any focusable canvas object that can accept desktop key events. +// This is the traditional key down and up event that is not applicable to all devices. +type Keyable interface { + fyne.Focusable + + KeyDown(*fyne.KeyEvent) + KeyUp(*fyne.KeyEvent) +} diff --git a/vendor/fyne.io/fyne/v2/driver/desktop/mouse.go b/vendor/fyne.io/fyne/v2/driver/desktop/mouse.go new file mode 100644 index 0000000..2bf256b --- /dev/null +++ b/vendor/fyne.io/fyne/v2/driver/desktop/mouse.go @@ -0,0 +1,58 @@ +package desktop + +import "fyne.io/fyne/v2" + +// MouseButton represents a single button in a desktop MouseEvent +type MouseButton int + +const ( + // MouseButtonPrimary is the most common mouse button - on some systems the only one. + // This will normally be on the left side of a mouse. + // + // Since: 2.0 + MouseButtonPrimary MouseButton = 1 << iota + + // MouseButtonSecondary is the secondary button on most mouse input devices. + // This will normally be on the right side of a mouse. + // + // Since: 2.0 + MouseButtonSecondary + + // MouseButtonTertiary is the middle button on the mouse, assuming it has one. + // + // Since: 2.0 + MouseButtonTertiary + + // LeftMouseButton is the most common mouse button - on some systems the only one. + // + // Deprecated: use MouseButtonPrimary which will adapt to mouse configuration. + LeftMouseButton = MouseButtonPrimary + + // RightMouseButton is the secondary button on most mouse input devices. + // + // Deprecated: use MouseButtonSecondary which will adapt to mouse configuration. + RightMouseButton = MouseButtonSecondary +) + +// MouseEvent contains data relating to desktop mouse events +type MouseEvent struct { + fyne.PointEvent + Button MouseButton + Modifier fyne.KeyModifier +} + +// Mouseable represents desktop mouse events that can be sent to CanvasObjects +type Mouseable interface { + MouseDown(*MouseEvent) + MouseUp(*MouseEvent) +} + +// Hoverable is used when a canvas object wishes to know if a pointer device moves over it. +type Hoverable interface { + // MouseIn is a hook that is called if the mouse pointer enters the element. + MouseIn(*MouseEvent) + // MouseMoved is a hook that is called if the mouse pointer moved over the element. + MouseMoved(*MouseEvent) + // MouseOut is a hook that is called if the mouse pointer leaves the element. + MouseOut() +} diff --git a/vendor/fyne.io/fyne/v2/driver/desktop/shortcut.go b/vendor/fyne.io/fyne/v2/driver/desktop/shortcut.go new file mode 100644 index 0000000..facb547 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/driver/desktop/shortcut.go @@ -0,0 +1,58 @@ +package desktop + +import ( + "runtime" + "strings" + + "fyne.io/fyne/v2" +) + +// Declare conformity with Shortcut interface +var ( + _ fyne.Shortcut = (*CustomShortcut)(nil) + _ fyne.KeyboardShortcut = (*CustomShortcut)(nil) +) + +// CustomShortcut describes a shortcut desktop event. +type CustomShortcut struct { + fyne.KeyName + Modifier fyne.KeyModifier +} + +// Key returns the key name of this shortcut. +func (cs *CustomShortcut) Key() fyne.KeyName { + return cs.KeyName +} + +// Mod returns the modifier of this shortcut. +func (cs *CustomShortcut) Mod() fyne.KeyModifier { + return cs.Modifier +} + +// ShortcutName returns the shortcut name associated to the event. +func (cs *CustomShortcut) ShortcutName() string { + id := &strings.Builder{} + id.WriteString("CustomDesktop:") + writeModifiers(id, cs.Modifier) + id.WriteString(string(cs.KeyName)) + return id.String() +} + +func writeModifiers(w *strings.Builder, mods fyne.KeyModifier) { + if (mods & fyne.KeyModifierShift) != 0 { + w.WriteString("Shift+") + } + if (mods & fyne.KeyModifierControl) != 0 { + w.WriteString("Control+") + } + if (mods & fyne.KeyModifierAlt) != 0 { + w.WriteString("Alt+") + } + if (mods & fyne.KeyModifierSuper) != 0 { + if runtime.GOOS == "darwin" { + w.WriteString("Command+") + } else { + w.WriteString("Super+") + } + } +} diff --git a/vendor/fyne.io/fyne/v2/driver/embedded/driver.go b/vendor/fyne.io/fyne/v2/driver/embedded/driver.go new file mode 100644 index 0000000..9925669 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/driver/embedded/driver.go @@ -0,0 +1,19 @@ +package embedded + +import ( + "image" + + "fyne.io/fyne/v2" +) + +// Driver is an embedded driver designed for handling custom hardware. +// Various standard driver implementations are available in the fyne-x project. +// +// Since: 2.7 +type Driver interface { + Render(image.Image) + Run(func()) + + ScreenSize() fyne.Size + Queue() chan Event +} diff --git a/vendor/fyne.io/fyne/v2/driver/embedded/event.go b/vendor/fyne.io/fyne/v2/driver/embedded/event.go new file mode 100644 index 0000000..0460277 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/driver/embedded/event.go @@ -0,0 +1,8 @@ +package embedded + +// Event is the general type of all embedded device events. +// +// Since: 2.7 +type Event interface { + isEvent() +} diff --git a/vendor/fyne.io/fyne/v2/driver/embedded/keyboard.go b/vendor/fyne.io/fyne/v2/driver/embedded/keyboard.go new file mode 100644 index 0000000..ec02e86 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/driver/embedded/keyboard.go @@ -0,0 +1,39 @@ +package embedded + +import "fyne.io/fyne/v2" + +// KeyDirection specifies the press/release of a key event +// +// Since: 2.7 +type KeyDirection uint8 + +const ( + // KeyPressed specifies that a key was pushed down. + // + // Since: 2.7 + KeyPressed KeyDirection = iota + + // KeyReleased indicates a key was let back up. + // + // Since: 2.7 + KeyReleased +) + +// KeyEvent is an event from keyboard actions occurring in an embedded device keyboard. +// +// Since: 2.7 +type KeyEvent struct { + Name fyne.KeyName + Direction KeyDirection +} + +func (d *KeyEvent) isEvent() {} + +// CharacterEvent is an event specifying that a character was created by a hardware or virtual keyboard. +// +// Since: 2.7 +type CharacterEvent struct { + Rune rune +} + +func (c *CharacterEvent) isEvent() {} diff --git a/vendor/fyne.io/fyne/v2/driver/embedded/touch.go b/vendor/fyne.io/fyne/v2/driver/embedded/touch.go new file mode 100644 index 0000000..ac053cc --- /dev/null +++ b/vendor/fyne.io/fyne/v2/driver/embedded/touch.go @@ -0,0 +1,33 @@ +package embedded + +import "fyne.io/fyne/v2" + +// TouchDownEvent is for indicating that an embedded device touch screen or pointing device was pressed. +// +// Since: 2.7 +type TouchDownEvent struct { + Position fyne.Position + ID int +} + +func (t *TouchDownEvent) isEvent() {} + +// TouchMoveEvent is for indicating that an embedded device touch screen or pointing device was moved whilst being pressed. +// +// Since: 2.7 +type TouchMoveEvent struct { + Position fyne.Position + ID int +} + +func (t *TouchMoveEvent) isEvent() {} + +// TouchUpEvent is for indicating that an embedded device touch screen or pointing device was released. +// +// Since: 2.7 +type TouchUpEvent struct { + Position fyne.Position + ID int +} + +func (t *TouchUpEvent) isEvent() {} diff --git a/vendor/fyne.io/fyne/v2/driver/mobile/device.go b/vendor/fyne.io/fyne/v2/driver/mobile/device.go new file mode 100644 index 0000000..4444628 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/driver/mobile/device.go @@ -0,0 +1,12 @@ +// Package mobile provides mobile specific driver functionality. +package mobile + +// Device describes functionality only available on mobile +type Device interface { + // Request that the mobile device show the touch screen keyboard (standard layout) + ShowVirtualKeyboard() + // Request that the mobile device show the touch screen keyboard (custom layout) + ShowVirtualKeyboardType(KeyboardType) + // Request that the mobile device dismiss the touch screen keyboard + HideVirtualKeyboard() +} diff --git a/vendor/fyne.io/fyne/v2/driver/mobile/driver.go b/vendor/fyne.io/fyne/v2/driver/mobile/driver.go new file mode 100644 index 0000000..b0781fa --- /dev/null +++ b/vendor/fyne.io/fyne/v2/driver/mobile/driver.go @@ -0,0 +1,10 @@ +// Package mobile provides desktop specific mobile functionality. +package mobile + +// Driver represents the extended capabilities of a mobile driver +// +// Since: 2.4 +type Driver interface { + // GoBack asks the OS to go to the previous app / activity, where supported + GoBack() +} diff --git a/vendor/fyne.io/fyne/v2/driver/mobile/key.go b/vendor/fyne.io/fyne/v2/driver/mobile/key.go new file mode 100644 index 0000000..056b90a --- /dev/null +++ b/vendor/fyne.io/fyne/v2/driver/mobile/key.go @@ -0,0 +1,10 @@ +package mobile + +import ( + "fyne.io/fyne/v2" +) + +const ( + // KeyBack represents the back button which may be hardware or software + KeyBack fyne.KeyName = "Back" +) diff --git a/vendor/fyne.io/fyne/v2/driver/mobile/keyboard.go b/vendor/fyne.io/fyne/v2/driver/mobile/keyboard.go new file mode 100644 index 0000000..605c065 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/driver/mobile/keyboard.go @@ -0,0 +1,26 @@ +package mobile + +import ( + "fyne.io/fyne/v2" +) + +// KeyboardType represents a type of virtual keyboard +type KeyboardType int32 + +const ( + // DefaultKeyboard is the keyboard with default input style and "return" return key + DefaultKeyboard KeyboardType = iota + // SingleLineKeyboard is the keyboard with default input style and "Done" return key + SingleLineKeyboard + // NumberKeyboard is the keyboard with number input style and "Done" return key + NumberKeyboard + // PasswordKeyboard is used to ensure that text is not leaked to 3rd party keyboard providers + PasswordKeyboard +) + +// Keyboardable describes any CanvasObject that needs a keyboard +type Keyboardable interface { + fyne.Focusable + + Keyboard() KeyboardType +} diff --git a/vendor/fyne.io/fyne/v2/driver/mobile/touch.go b/vendor/fyne.io/fyne/v2/driver/mobile/touch.go new file mode 100644 index 0000000..3c11f1b --- /dev/null +++ b/vendor/fyne.io/fyne/v2/driver/mobile/touch.go @@ -0,0 +1,15 @@ +package mobile + +import "fyne.io/fyne/v2" + +// TouchEvent contains data relating to mobile touch events +type TouchEvent struct { + fyne.PointEvent +} + +// Touchable represents mobile touch events that can be sent to CanvasObjects +type Touchable interface { + TouchDown(*TouchEvent) + TouchUp(*TouchEvent) + TouchCancel(*TouchEvent) +} diff --git a/vendor/fyne.io/fyne/v2/driver/native.go b/vendor/fyne.io/fyne/v2/driver/native.go new file mode 100644 index 0000000..31b575d --- /dev/null +++ b/vendor/fyne.io/fyne/v2/driver/native.go @@ -0,0 +1,71 @@ +package driver + +// NativeWindow is an extension interface for `fyne.Window` that gives access +// to platform-native features of application windows. +// +// Since: 2.5 +type NativeWindow interface { + // RunNative provides a way to execute code within the platform-specific runtime context for a window. + // The context types are defined in the `driver` package and the specific context passed will differ by platform. + RunNative(func(context any)) +} + +// AndroidContext is passed to the RunNative callback when it is executed on an Android device. +// The VM, Env and Ctx pointers are required to make various calls into JVM methods. +// +// Since: 2.3 +type AndroidContext struct { + VM, Env, Ctx uintptr +} + +// AndroidWindowContext is passed to the NativeWindow.RunNative callback when it is executed +// on an Android device. The NativeWindow field is of type `*C.ANativeWindow`. +// The VM, Env and Ctx pointers are required to make various calls into JVM methods. +// +// Since: 2.5 +type AndroidWindowContext struct { + AndroidContext + NativeWindow uintptr +} + +// UnknownContext is passed to the RunNative callback when it is executed +// on devices or windows without special native context. +// +// Since: 2.3 +type UnknownContext struct{} + +// WindowsWindowContext is passed to the NativeWindow.RunNative callback +// when it is executed on a Microsoft Windows device. +// +// Since: 2.5 +type WindowsWindowContext struct { + // HWND is the window handle for the native window. + HWND uintptr +} + +// MacWindowContext is passed to the NativeWindow.RunNative callback +// when it is executed on a macOS device. +// +// Since: 2.5 +type MacWindowContext struct { + // NSWindow is the window handle for the native window. + NSWindow uintptr +} + +// X11WindowContext is passed to the NativeWindow.RunNative callback +// when it is executed on a device with the X11 windowing system. +// +// Since: 2.5 +type X11WindowContext struct { + // WindowHandle is the window handle for the native X11 window. + WindowHandle uintptr +} + +// WaylandWindowContext is passed to the NativeWindow.RunNative callback +// when it is executed on a device with the Wayland windowing system. +// +// Since: 2.5 +type WaylandWindowContext struct { + // WaylandSurface is the handle to the native Wayland surface. + WaylandSurface uintptr +} diff --git a/vendor/fyne.io/fyne/v2/driver/native_android.go b/vendor/fyne.io/fyne/v2/driver/native_android.go new file mode 100644 index 0000000..64cae33 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/driver/native_android.go @@ -0,0 +1,16 @@ +//go:build android + +package driver + +import "fyne.io/fyne/v2/internal/driver/mobile/app" + +// RunNative provides a way to execute code within the platform-specific runtime context for various runtimes. +// On Android this provides the JVM pointers required to execute various NDK calls or use JNI APIs. +// +// Since: 2.3 +func RunNative(fn func(any) error) error { + return app.RunOnJVM(func(vm, env, ctx uintptr) error { + data := &AndroidContext{VM: vm, Env: env, Ctx: ctx} + return fn(data) + }) +} diff --git a/vendor/fyne.io/fyne/v2/driver/native_other.go b/vendor/fyne.io/fyne/v2/driver/native_other.go new file mode 100644 index 0000000..0f61efb --- /dev/null +++ b/vendor/fyne.io/fyne/v2/driver/native_other.go @@ -0,0 +1,12 @@ +//go:build !android + +package driver + +// RunNative provides a way to execute code within the platform-specific runtime context for various runtimes. +// This is mostly useful for Android where the JVM provides functionality that is not accessible directly in CGo. +// The call for most platforms will just execute passing an `UnknownContext` and returning any error reported. +// +// Since: 2.3 +func RunNative(fn func(any) error) error { + return fn(&UnknownContext{}) +} diff --git a/vendor/fyne.io/fyne/v2/driver/software/render.go b/vendor/fyne.io/fyne/v2/driver/software/render.go new file mode 100644 index 0000000..2a22d43 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/driver/software/render.go @@ -0,0 +1,31 @@ +package software + +import ( + "image" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/app" +) + +// RenderCanvas takes a canvas and renders it to a regular Go image using the provided Theme. +// This is the same as setting the application theme and then calling Canvas.Capture(). +func RenderCanvas(c fyne.Canvas, t fyne.Theme) image.Image { + fyne.CurrentApp().Settings().SetTheme(t) + app.ApplyThemeTo(c.Content(), c) + + return c.Capture() +} + +// Render takes a canvas object and renders it to a regular Go image using the provided Theme. +// The returned image will be set to the object's minimum size. +// Use the theme.LightTheme() or theme.DarkTheme() to access the builtin themes. +func Render(obj fyne.CanvasObject, t fyne.Theme) image.Image { + fyne.CurrentApp().Settings().SetTheme(t) + + c := NewCanvas() + c.SetPadded(false) + c.SetContent(obj) + + app.ApplyThemeTo(obj, c) + return c.Capture() +} diff --git a/vendor/fyne.io/fyne/v2/driver/software/softwarecanvas.go b/vendor/fyne.io/fyne/v2/driver/software/softwarecanvas.go new file mode 100644 index 0000000..2112cff --- /dev/null +++ b/vendor/fyne.io/fyne/v2/driver/software/softwarecanvas.go @@ -0,0 +1,18 @@ +package software + +import ( + "fyne.io/fyne/v2/internal/painter/software" + "fyne.io/fyne/v2/test" +) + +// NewCanvas creates a new canvas in memory that can render without hardware support. +func NewCanvas() test.WindowlessCanvas { + return test.NewCanvasWithPainter(software.NewPainter()) +} + +// NewTransparentCanvas creates a new canvas in memory that can render without hardware support without a background color. +// +// Since: 2.2 +func NewTransparentCanvas() test.WindowlessCanvas { + return test.NewTransparentCanvasWithPainter(software.NewPainter()) +} diff --git a/vendor/fyne.io/fyne/v2/event.go b/vendor/fyne.io/fyne/v2/event.go new file mode 100644 index 0000000..0f00605 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/event.go @@ -0,0 +1,37 @@ +package fyne + +// HardwareKey contains information associated with physical key events +// Most applications should use [KeyName] for cross-platform compatibility. +type HardwareKey struct { + // ScanCode represents a hardware ID for (normally desktop) keyboard events. + ScanCode int +} + +// KeyEvent describes a keyboard input event. +type KeyEvent struct { + // Name describes the keyboard event that is consistent across platforms. + Name KeyName + // Physical is a platform specific field that reports the hardware information of physical keyboard events. + Physical HardwareKey +} + +// PointEvent describes a pointer input event. The position is relative to the +// top-left of the [CanvasObject] this is triggered on. +type PointEvent struct { + AbsolutePosition Position // The absolute position of the event + Position Position // The relative position of the event +} + +// ScrollEvent defines the parameters of a pointer or other scroll event. +// The DeltaX and DeltaY represent how large the scroll was in two dimensions. +type ScrollEvent struct { + PointEvent + Scrolled Delta +} + +// DragEvent defines the parameters of a pointer or other drag event. +// The DraggedX and DraggedY fields show how far the item was dragged since the last event. +type DragEvent struct { + PointEvent + Dragged Delta +} diff --git a/vendor/fyne.io/fyne/v2/fyne.go b/vendor/fyne.io/fyne/v2/fyne.go new file mode 100644 index 0000000..8975b89 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/fyne.go @@ -0,0 +1,28 @@ +// Package fyne describes the objects and components available to any Fyne app. +// These can all be created, manipulated and tested without rendering (for speed). +// Your main package should use the app package to create an application with +// a default driver that will render your UI. +// +// A simple application may look like this: +// +// package main +// +// import "fyne.io/fyne/v2/app" +// import "fyne.io/fyne/v2/container" +// import "fyne.io/fyne/v2/widget" +// +// func main() { +// a := app.New() +// w := a.NewWindow("Hello") +// +// hello := widget.NewLabel("Hello Fyne!") +// w.SetContent(container.NewVBox( +// hello, +// widget.NewButton("Hi!", func() { +// hello.SetText("Welcome :)") +// }), +// )) +// +// w.ShowAndRun() +// } +package fyne // import "fyne.io/fyne/v2" diff --git a/vendor/fyne.io/fyne/v2/geometry.go b/vendor/fyne.io/fyne/v2/geometry.go new file mode 100644 index 0000000..e0dec55 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/geometry.go @@ -0,0 +1,162 @@ +package fyne + +var ( + _ Vector2 = (*Delta)(nil) + _ Vector2 = (*Position)(nil) + _ Vector2 = (*Size)(nil) +) + +// Vector2 marks geometry types that can operate as a coordinate vector. +type Vector2 interface { + Components() (float32, float32) + IsZero() bool +} + +// Delta is a generic X, Y coordinate, size or movement representation. +type Delta struct { + DX, DY float32 +} + +// NewDelta returns a newly allocated [Delta] representing a movement in the X and Y axis. +func NewDelta(dx float32, dy float32) Delta { + return Delta{DX: dx, DY: dy} +} + +// Components returns the X and Y elements of v. +func (v Delta) Components() (float32, float32) { + return v.DX, v.DY +} + +// IsZero returns whether the Position is at the zero-point. +func (v Delta) IsZero() bool { + return v.DX == 0.0 && v.DY == 0.0 +} + +// Position describes a generic X, Y coordinate relative to a parent [Canvas] +// or [CanvasObject]. +type Position struct { + X float32 // The position from the parent's left edge + Y float32 // The position from the parent's top edge +} + +// NewPos returns a newly allocated [Position] representing the specified coordinates. +func NewPos(x float32, y float32) Position { + return Position{x, y} +} + +// NewSquareOffsetPos returns a newly allocated [Position] with the same x and y position. +// +// Since: 2.4 +func NewSquareOffsetPos(length float32) Position { + return Position{length, length} +} + +// Add returns a new [Position] that is the result of offsetting the current +// position by p2 X and Y. +func (p Position) Add(v Vector2) Position { + // NOTE: Do not simplify to `return p.AddXY(v.Components())`, it prevents inlining. + x, y := v.Components() + return Position{p.X + x, p.Y + y} +} + +// AddXY returns a new [Position] by adding x and y to the current one. +func (p Position) AddXY(x, y float32) Position { + return Position{p.X + x, p.Y + y} +} + +// Components returns the X and Y elements of p. +func (p Position) Components() (float32, float32) { + return p.X, p.Y +} + +// IsZero returns whether the Position is at the zero-point. +func (p Position) IsZero() bool { + return p.X == 0.0 && p.Y == 0.0 +} + +// Subtract returns a new [Position] that is the result of offsetting the current +// position by p2 -X and -Y. +func (p Position) Subtract(v Vector2) Position { + // NOTE: Do not simplify to `return p.SubtractXY(v.Components())`, it prevents inlining. + x, y := v.Components() + return Position{p.X - x, p.Y - y} +} + +// SubtractXY returns a new [Position] by subtracting x and y from the current one. +func (p Position) SubtractXY(x, y float32) Position { + return Position{p.X - x, p.Y - y} +} + +// Size describes something with width and height. +type Size struct { + Width float32 // The number of units along the X axis. + Height float32 // The number of units along the Y axis. +} + +// NewSize returns a newly allocated Size of the specified dimensions. +func NewSize(w float32, h float32) Size { + return Size{w, h} +} + +// NewSquareSize returns a newly allocated Size with the same width and height. +// +// Since: 2.4 +func NewSquareSize(side float32) Size { + return Size{side, side} +} + +// Add returns a new Size that is the result of increasing the current size by +// s2 Width and Height. +func (s Size) Add(v Vector2) Size { + // NOTE: Do not simplify to `return s.AddXY(v.Components())`, it prevents inlining. + w, h := v.Components() + return Size{s.Width + w, s.Height + h} +} + +// AddWidthHeight returns a new Size by adding width and height to the current one. +func (s Size) AddWidthHeight(width, height float32) Size { + return Size{s.Width + width, s.Height + height} +} + +// IsZero returns whether the Size has zero width and zero height. +func (s Size) IsZero() bool { + return s.Width == 0.0 && s.Height == 0.0 +} + +// Max returns a new [Size] that is the maximum of the current Size and s2. +func (s Size) Max(v Vector2) Size { + x, y := v.Components() + + maxW := Max(s.Width, x) + maxH := Max(s.Height, y) + + return NewSize(maxW, maxH) +} + +// Min returns a new [Size] that is the minimum of s and v. +func (s Size) Min(v Vector2) Size { + x, y := v.Components() + + minW := Min(s.Width, x) + minH := Min(s.Height, y) + + return NewSize(minW, minH) +} + +// Components returns the Width and Height elements of this Size +func (s Size) Components() (float32, float32) { + return s.Width, s.Height +} + +// Subtract returns a new Size that is the result of decreasing the current size +// by s2 Width and Height. +func (s Size) Subtract(v Vector2) Size { + // NOTE: Do not simplify to `return s.SubtractXY(v.Components())`, it prevents inlining. + w, h := v.Components() + return Size{s.Width - w, s.Height - h} +} + +// SubtractWidthHeight returns a new Size by subtracting width and height from the current one. +func (s Size) SubtractWidthHeight(width, height float32) Size { + return Size{s.Width - width, s.Height - height} +} diff --git a/vendor/fyne.io/fyne/v2/internal/animation/animation.go b/vendor/fyne.io/fyne/v2/internal/animation/animation.go new file mode 100644 index 0000000..7570da3 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/animation/animation.go @@ -0,0 +1,32 @@ +package animation + +import ( + "time" + + "fyne.io/fyne/v2" +) + +type anim struct { + a *fyne.Animation + end time.Time + repeatsLeft int + reverse bool + start time.Time + total int64 + stopped bool +} + +func newAnim(a *fyne.Animation) *anim { + animate := &anim{a: a, start: time.Now(), end: time.Now().Add(a.Duration)} + animate.total = animate.end.Sub(animate.start).Milliseconds() + animate.repeatsLeft = a.RepeatCount + return animate +} + +func (a *anim) setStopped() { + a.stopped = true +} + +func (a *anim) isStopped() bool { + return a.stopped +} diff --git a/vendor/fyne.io/fyne/v2/internal/animation/runner.go b/vendor/fyne.io/fyne/v2/internal/animation/runner.go new file mode 100644 index 0000000..0acdb71 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/animation/runner.go @@ -0,0 +1,174 @@ +package animation + +import ( + "sync" + "time" + + "fyne.io/fyne/v2" +) + +// Runner is the main driver for animations package +type Runner struct { + // animationMutex synchronizes access to `animations` and `pendingAnimations` + // between the runner goroutine and calls to Start and Stop + animationMutex sync.RWMutex + + // animations is the list of animations that are being ticked in the current frame + animations []*anim + + // pendingAnimations is animations that have been started but not yet picked up + // by the runner goroutine to be ticked each frame + pendingAnimations []*anim + + // nextFrameAnimations is the list of animations that will be ticked in the next frame. + // It is accessed only by the runner goroutine and accumulates the continuing animations + // during a tick that are not completed, plus the pendingAnimations picked up at the end of the frame. + // At the end of a full frame of animations, the nextFrameAnimations slice is swapped with + // the current `animations` slice which is then cleared out, while holding the mutex. + nextFrameAnimations []*anim + + runnerStarted bool +} + +// Start will register the passed application and initiate its ticking. +func (r *Runner) Start(a *fyne.Animation) { + r.animationMutex.Lock() + defer r.animationMutex.Unlock() + + if !r.runnerStarted { + r.runnerStarted = true + if r.animations == nil { + // initialize with excess capacity to avoid re-allocations + // on subsequent Starts + r.animations = make([]*anim, 0, 16) + } + r.animations = append(r.animations, newAnim(a)) + } else { + if r.pendingAnimations == nil { + // initialize with excess capacity to avoid re-allocations + // on subsequent Starts + r.pendingAnimations = make([]*anim, 0, 16) + } + r.pendingAnimations = append(r.pendingAnimations, newAnim(a)) + } +} + +// Stop causes an animation to stop ticking (if it was still running) and removes it from the runner. +func (r *Runner) Stop(a *fyne.Animation) { + r.animationMutex.Lock() + defer r.animationMutex.Unlock() + + newList := make([]*anim, 0, len(r.animations)) + stopped := false + for _, item := range r.animations { + if item.a != a { + newList = append(newList, item) + } else { + item.setStopped() + stopped = true + } + } + r.animations = newList + if stopped { + return + } + + newList = make([]*anim, 0, len(r.pendingAnimations)) + for _, item := range r.pendingAnimations { + if item.a != a { + newList = append(newList, item) + } else { + item.setStopped() + } + } + r.pendingAnimations = newList +} + +// TickAnimations progresses all running animations by one tick. +// This will be called from the driver to update objects immediately before next paint. +func (r *Runner) TickAnimations() { + if !r.runnerStarted { + return + } + + done := r.runOneFrame() + + if done { + r.animationMutex.Lock() + r.runnerStarted = false + r.animationMutex.Unlock() + } +} + +func (r *Runner) runOneFrame() (done bool) { + r.animationMutex.Lock() + oldList := r.animations + r.animationMutex.Unlock() + for _, a := range oldList { + if !a.isStopped() && r.tickAnimation(a) { + r.nextFrameAnimations = append(r.nextFrameAnimations, a) + } + } + + r.animationMutex.Lock() + // nil out old r.animations for re-use as next r.nextFrameAnimations + tmp := r.animations + for i := range tmp { + tmp[i] = nil + } + r.animations = append(r.nextFrameAnimations, r.pendingAnimations...) + r.nextFrameAnimations = tmp[:0] + // nil out r.pendingAnimations + for i := range r.pendingAnimations { + r.pendingAnimations[i] = nil + } + r.pendingAnimations = r.pendingAnimations[:0] + done = len(r.animations) == 0 + r.animationMutex.Unlock() + return done +} + +// tickAnimation will process a frame of animation and return true if this should continue animating +func (r *Runner) tickAnimation(a *anim) bool { + if time.Now().After(a.end) { + if a.reverse { + a.a.Tick(0.0) + if a.repeatsLeft == 0 { + return false + } + a.reverse = false + } else { + a.a.Tick(1.0) + if a.a.AutoReverse { + a.reverse = true + } + } + if !a.reverse { + if a.repeatsLeft == 0 { + return false + } + if a.repeatsLeft > 0 { + a.repeatsLeft-- + } + } + + a.start = time.Now() + a.end = a.start.Add(a.a.Duration) + return true + } + + delta := time.Since(a.start).Milliseconds() + + val := float32(delta) / float32(a.total) + curve := a.a.Curve + if curve == nil { + curve = fyne.AnimationEaseInOut + } + if a.reverse { + a.a.Tick(curve(1 - val)) + } else { + a.a.Tick(curve(val)) + } + + return true +} diff --git a/vendor/fyne.io/fyne/v2/internal/app/config.go b/vendor/fyne.io/fyne/v2/internal/app/config.go new file mode 100644 index 0000000..bb31841 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/app/config.go @@ -0,0 +1,5 @@ +package app + +func RootConfigDir() string { + return rootConfigDir() +} diff --git a/vendor/fyne.io/fyne/v2/internal/app/config_desktop_darwin.go b/vendor/fyne.io/fyne/v2/internal/app/config_desktop_darwin.go new file mode 100644 index 0000000..bde0b12 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/app/config_desktop_darwin.go @@ -0,0 +1,15 @@ +//go:build !ci && !ios && !wasm && !test_web_driver && !mobile && !noos && !tinygo + +package app + +import ( + "os" + "path/filepath" +) + +func rootConfigDir() string { + homeDir, _ := os.UserHomeDir() + + desktopConfig := filepath.Join(filepath.Join(homeDir, "Library"), "Preferences") + return filepath.Join(desktopConfig, "fyne") +} diff --git a/vendor/fyne.io/fyne/v2/internal/app/config_mobile_and.go b/vendor/fyne.io/fyne/v2/internal/app/config_mobile_and.go new file mode 100644 index 0000000..c4f4ca5 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/app/config_mobile_and.go @@ -0,0 +1,19 @@ +//go:build !ci && android && !noos && !tinygo + +package app + +import ( + "log" + "os" + "path/filepath" +) + +func rootConfigDir() string { + filesDir := os.Getenv("FILESDIR") + if filesDir == "" { + log.Println("FILESDIR env was not set by android native code") + return "/data/data" // probably won't work, but we can't make a better guess + } + + return filepath.Join(filesDir, "fyne") +} diff --git a/vendor/fyne.io/fyne/v2/internal/app/config_mobile_ios.go b/vendor/fyne.io/fyne/v2/internal/app/config_mobile_ios.go new file mode 100644 index 0000000..5bbefdb --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/app/config_mobile_ios.go @@ -0,0 +1,19 @@ +//go:build !ci && ios && !mobile && !noos && !tinygo + +package app + +import ( + "path/filepath" +) + +/* +#include + +char *documentsPath(void); +*/ +import "C" + +func rootConfigDir() string { + root := C.documentsPath() + return filepath.Join(C.GoString(root), "fyne") +} diff --git a/vendor/fyne.io/fyne/v2/internal/app/config_mobile_ios.m b/vendor/fyne.io/fyne/v2/internal/app/config_mobile_ios.m new file mode 100644 index 0000000..059005a --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/app/config_mobile_ios.m @@ -0,0 +1,9 @@ +//go:build !ci && ios + +#import + +char *documentsPath() { + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + NSString *path = paths.firstObject; + return [path UTF8String]; +} \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/internal/app/config_noos.go b/vendor/fyne.io/fyne/v2/internal/app/config_noos.go new file mode 100644 index 0000000..97924a4 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/app/config_noos.go @@ -0,0 +1,13 @@ +//go:build noos || tinygo + +package app + +import ( + "os" + "path/filepath" +) + +func rootConfigDir() string { + home, _ := os.UserHomeDir() + return filepath.Join(home, ".config", "fyne") +} diff --git a/vendor/fyne.io/fyne/v2/internal/app/config_other.go b/vendor/fyne.io/fyne/v2/internal/app/config_other.go new file mode 100644 index 0000000..b1dc980 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/app/config_other.go @@ -0,0 +1,12 @@ +//go:build ci || (mobile && !android && !ios) || (!linux && !darwin && !windows && !freebsd && !openbsd && !netbsd && !wasm && !test_web_driver && !noos && !tinygo) + +package app + +import ( + "os" + "path/filepath" +) + +func rootConfigDir() string { + return filepath.Join(os.TempDir(), "fyne-test") +} diff --git a/vendor/fyne.io/fyne/v2/internal/app/config_wasm.go b/vendor/fyne.io/fyne/v2/internal/app/config_wasm.go new file mode 100644 index 0000000..8463675 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/app/config_wasm.go @@ -0,0 +1,7 @@ +//go:build !ci && (!android || !ios || !mobile) && (wasm || test_web_driver) && !noos && !tinygo + +package app + +func rootConfigDir() string { + return "/data/" +} diff --git a/vendor/fyne.io/fyne/v2/internal/app/config_windows.go b/vendor/fyne.io/fyne/v2/internal/app/config_windows.go new file mode 100644 index 0000000..88e4b1e --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/app/config_windows.go @@ -0,0 +1,15 @@ +//go:build !ci && !android && !ios && !wasm && !test_web_driver && !noos && !tinygo + +package app + +import ( + "os" + "path/filepath" +) + +func rootConfigDir() string { + homeDir, _ := os.UserHomeDir() + + desktopConfig := filepath.Join(filepath.Join(homeDir, "AppData"), "Roaming") + return filepath.Join(desktopConfig, "fyne") +} diff --git a/vendor/fyne.io/fyne/v2/internal/app/config_xdg.go b/vendor/fyne.io/fyne/v2/internal/app/config_xdg.go new file mode 100644 index 0000000..5214fde --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/app/config_xdg.go @@ -0,0 +1,13 @@ +//go:build !ci && !wasm && !test_web_driver && !android && !ios && !mobile && (linux || openbsd || freebsd || netbsd) && !noos && !tinygo + +package app + +import ( + "os" + "path/filepath" +) + +func rootConfigDir() string { + desktopConfig, _ := os.UserConfigDir() + return filepath.Join(desktopConfig, "fyne") +} diff --git a/vendor/fyne.io/fyne/v2/internal/app/focus_manager.go b/vendor/fyne.io/fyne/v2/internal/app/focus_manager.go new file mode 100644 index 0000000..63fbb3e --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/app/focus_manager.go @@ -0,0 +1,150 @@ +package app + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/driver" +) + +// FocusManager represents a standard manager of input focus for a canvas +type FocusManager struct { + content fyne.CanvasObject + focused fyne.Focusable +} + +// NewFocusManager returns a new instance of the standard focus manager for a canvas. +func NewFocusManager(c fyne.CanvasObject) *FocusManager { + return &FocusManager{content: c} +} + +// Focus focuses the given obj. +func (f *FocusManager) Focus(obj fyne.Focusable) bool { + if obj != nil { + var hiddenAncestor fyne.CanvasObject + hidden := false + found := driver.WalkCompleteObjectTree( + f.content, + func(object fyne.CanvasObject, _, _ fyne.Position, _ fyne.Size) bool { + if hiddenAncestor == nil && !object.Visible() { + hiddenAncestor = object + } + if object == obj.(fyne.CanvasObject) { + hidden = hiddenAncestor != nil + return true + } + return false + }, + func(object fyne.CanvasObject, pos fyne.Position, _ fyne.CanvasObject) { + if hiddenAncestor == object { + hiddenAncestor = nil + } + }, + ) + if !found { + return false + } + if hidden { + return true + } + if dis, ok := obj.(fyne.Disableable); ok && dis.Disabled() { + type selectableText interface { + SelectedText() string + } + if _, isSelectableText := obj.(selectableText); !isSelectableText || fyne.CurrentDevice().IsMobile() { + return true + } + } + } + f.focus(obj) + return true +} + +// Focused returns the currently focused object or nil if none. +func (f *FocusManager) Focused() fyne.Focusable { + return f.focused +} + +// FocusGained signals to the manager that its content got focus (due to window/overlay switch for instance). +func (f *FocusManager) FocusGained() { + if focused := f.Focused(); focused != nil { + focused.FocusGained() + } +} + +// FocusLost signals to the manager that its content lost focus (due to window/overlay switch for instance). +func (f *FocusManager) FocusLost() { + if focused := f.Focused(); focused != nil { + focused.FocusLost() + } +} + +// FocusNext will find the item after the current that can be focused and focus it. +// If current is nil then the first focusable item in the canvas will be focused. +func (f *FocusManager) FocusNext() { + f.focus(f.nextInChain(f.focused)) +} + +// FocusPrevious will find the item before the current that can be focused and focus it. +// If current is nil then the last focusable item in the canvas will be focused. +func (f *FocusManager) FocusPrevious() { + f.focus(f.previousInChain(f.focused)) +} + +func (f *FocusManager) focus(obj fyne.Focusable) { + if f.focused == obj { + return + } + + if f.focused != nil { + f.focused.FocusLost() + } + f.focused = obj + if obj != nil { + obj.FocusGained() + } +} + +func (f *FocusManager) nextInChain(current fyne.Focusable) fyne.Focusable { + return f.nextWithWalker(current, driver.WalkVisibleObjectTree) +} + +func (f *FocusManager) nextWithWalker(current fyne.Focusable, walker walkerFunc) fyne.Focusable { + var next fyne.Focusable + found := current == nil // if we have no starting point then pretend we matched already + walker(f.content, func(obj fyne.CanvasObject, _ fyne.Position, _ fyne.Position, _ fyne.Size) bool { + if w, ok := obj.(fyne.Disableable); ok && w.Disabled() { + // disabled widget cannot receive focus + return false + } + + focus, ok := obj.(fyne.Focusable) + if !ok { + return false + } + + if found { + next = focus + return true + } + if next == nil { + next = focus + } + + if obj == current.(fyne.CanvasObject) { + found = true + } + + return false + }, nil) + + return next +} + +func (f *FocusManager) previousInChain(current fyne.Focusable) fyne.Focusable { + return f.nextWithWalker(current, driver.ReverseWalkVisibleObjectTree) +} + +type walkerFunc func( + fyne.CanvasObject, + func(fyne.CanvasObject, fyne.Position, fyne.Position, fyne.Size) bool, + func(fyne.CanvasObject, fyne.Position, fyne.CanvasObject), +) bool diff --git a/vendor/fyne.io/fyne/v2/internal/app/lifecycle.go b/vendor/fyne.io/fyne/v2/internal/app/lifecycle.go new file mode 100644 index 0000000..2a7375f --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/app/lifecycle.go @@ -0,0 +1,121 @@ +package app + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/async" +) + +var _ fyne.Lifecycle = (*Lifecycle)(nil) + +// Lifecycle represents the various phases that an app can transition through. +// +// Since: 2.1 +type Lifecycle struct { + onForeground func() + onBackground func() + onStarted func() + onStopped func() + + onStoppedHookExecuted func() + + eventQueue *async.UnboundedChan[func()] +} + +// SetOnStoppedHookExecuted is an internal function that lets Fyne schedule a clean-up after +// the user-provided stopped hook. It should only be called once during an application start-up. +func (l *Lifecycle) SetOnStoppedHookExecuted(f func()) { + l.onStoppedHookExecuted = f +} + +// SetOnEnteredForeground hooks into the app becoming foreground. +func (l *Lifecycle) SetOnEnteredForeground(f func()) { + l.onForeground = f +} + +// SetOnExitedForeground hooks into the app having moved to the background. +// Depending on the platform it may still be visible but will not receive keyboard events. +// On some systems hover or desktop mouse move events may still occur. +func (l *Lifecycle) SetOnExitedForeground(f func()) { + l.onBackground = f +} + +// SetOnStarted hooks into an event that says the app is now running. +func (l *Lifecycle) SetOnStarted(f func()) { + l.onStarted = f +} + +// SetOnStopped hooks into an event that says the app is no longer running. +func (l *Lifecycle) SetOnStopped(f func()) { + l.onStopped = f +} + +// OnEnteredForeground returns the focus gained hook, if one is registered. +func (l *Lifecycle) OnEnteredForeground() func() { + return l.onForeground +} + +// OnExitedForeground returns the focus lost hook, if one is registered. +func (l *Lifecycle) OnExitedForeground() func() { + return l.onBackground +} + +// OnStarted returns the started hook, if one is registered. +func (l *Lifecycle) OnStarted() func() { + return l.onStarted +} + +// OnStopped returns the stopped hook, if one is registered. +func (l *Lifecycle) OnStopped() func() { + stopped := l.onStopped + stopHook := l.onStoppedHookExecuted + if stopped == nil && stopHook == nil { + return nil + } + + if stopHook == nil { + return stopped + } + + if stopped == nil { + return stopHook + } + + // we have a stopped handle and the onStoppedHook + return func() { + stopped() + stopHook() + } +} + +// DestroyEventQueue destroys the event queue. +func (l *Lifecycle) DestroyEventQueue() { + l.eventQueue.Close() +} + +// InitEventQueue initializes the event queue. +func (l *Lifecycle) InitEventQueue() { + // This channel should be closed when the window is closed. + l.eventQueue = async.NewUnboundedChan[func()]() +} + +// QueueEvent uses this method to queue up a callback that handles an event. This ensures +// user interaction events for a given window are processed in order. +func (l *Lifecycle) QueueEvent(fn func()) { + l.eventQueue.In() <- fn +} + +// RunEventQueue runs the event queue. This should called inside a go routine. +// This function blocks. +func (l *Lifecycle) RunEventQueue(run func(func(), bool)) { + for fn := range l.eventQueue.Out() { + run(fn, true) + } +} + +// WaitForEvents wait for all the events. +func (l *Lifecycle) WaitForEvents() { + done := make(chan struct{}) + + l.eventQueue.In() <- func() { done <- struct{}{} } + <-done +} diff --git a/vendor/fyne.io/fyne/v2/internal/app/meta.go b/vendor/fyne.io/fyne/v2/internal/app/meta.go new file mode 100644 index 0000000..54a545b --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/app/meta.go @@ -0,0 +1,10 @@ +package app + +// these internal variables are set by the fyne build command so that the "FyneApp.toml" data is readable at runtime. +var ( + MetaIcon = "" // this will contain base64 encoded icon bytes + MetaID = "com.example" + MetaName = "Fyne App" + MetaVersion = "1.0.0" + MetaBuild = "1" +) diff --git a/vendor/fyne.io/fyne/v2/internal/app/theme.go b/vendor/fyne.io/fyne/v2/internal/app/theme.go new file mode 100644 index 0000000..626a341 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/app/theme.go @@ -0,0 +1,52 @@ +package app + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/cache" +) + +// ApplyThemeTo ensures that the specified canvasobject and all widgets and themeable objects will +// be updated for the current theme. +func ApplyThemeTo(content fyne.CanvasObject, canv fyne.Canvas) { + if content == nil { + return + } + + switch o := content.(type) { + case fyne.Widget: + renderer := cache.Renderer(o) + for _, co := range renderer.Objects() { + ApplyThemeTo(co, canv) + } + renderer.Layout(content.Size()) // theme can cause sizing changes + case *fyne.Container: + for _, co := range o.Objects { + ApplyThemeTo(co, canv) + } + if l := o.Layout; l != nil { + l.Layout(o.Objects, o.Size()) // theme can cause sizing changes + } + } + content.Refresh() +} + +// ApplySettings ensures that all widgets and themeable objects in an application will be updated for the current theme. +// It also checks that scale changes are reflected if required +func ApplySettings(set fyne.Settings, app fyne.App) { + ApplySettingsWithCallback(set, app, nil) +} + +// ApplySettingsWithCallback ensures that all widgets and themeable objects in an application will be updated for the current theme. +// It also checks that scale changes are reflected if required. Also it will call `onEveryWindow` on every window +// interaction +func ApplySettingsWithCallback(set fyne.Settings, app fyne.App, onEveryWindow func(w fyne.Window)) { + for _, window := range app.Driver().AllWindows() { + ApplyThemeTo(window.Content(), window.Canvas()) + for _, overlay := range window.Canvas().Overlays().List() { + ApplyThemeTo(overlay, window.Canvas()) + } + if onEveryWindow != nil { + onEveryWindow(window) + } + } +} diff --git a/vendor/fyne.io/fyne/v2/internal/app/theme_darwin.go b/vendor/fyne.io/fyne/v2/internal/app/theme_darwin.go new file mode 100644 index 0000000..538455c --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/app/theme_darwin.go @@ -0,0 +1,28 @@ +//go:build !ios && !wasm && !test_web_driver && !mobile + +package app + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Foundation + +#include + +bool isDarkMode(); +*/ +import "C" + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/theme" +) + +// DefaultVariant returns the systems default fyne.ThemeVariant. +// Normally, you should not need this. It is extracted out of the root app package to give the +// settings app access to it. +func DefaultVariant() fyne.ThemeVariant { + if C.isDarkMode() { + return theme.VariantDark + } + return theme.VariantLight +} diff --git a/vendor/fyne.io/fyne/v2/internal/app/theme_darwin.m b/vendor/fyne.io/fyne/v2/internal/app/theme_darwin.m new file mode 100644 index 0000000..a5ad06a --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/app/theme_darwin.m @@ -0,0 +1,8 @@ +//go:build !ios && !wasm && !test_web_driver && !mobile + +#import + +bool isDarkMode() { + NSString *style = [[NSUserDefaults standardUserDefaults] stringForKey:@"AppleInterfaceStyle"]; + return [@"Dark" isEqualToString:style]; +} diff --git a/vendor/fyne.io/fyne/v2/internal/app/theme_mobile.go b/vendor/fyne.io/fyne/v2/internal/app/theme_mobile.go new file mode 100644 index 0000000..fc10428 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/app/theme_mobile.go @@ -0,0 +1,18 @@ +//go:build android || ios || mobile + +package app + +import ( + "fyne.io/fyne/v2" +) + +// SystemTheme contains the system’s theme variant. +// It is intended for internal use, only! +var SystemTheme fyne.ThemeVariant + +// DefaultVariant returns the systems default fyne.ThemeVariant. +// Normally, you should not need this. It is extracted out of the root app package to give the +// settings app access to it. +func DefaultVariant() fyne.ThemeVariant { + return SystemTheme +} diff --git a/vendor/fyne.io/fyne/v2/internal/app/theme_other.go b/vendor/fyne.io/fyne/v2/internal/app/theme_other.go new file mode 100644 index 0000000..9a6cc4d --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/app/theme_other.go @@ -0,0 +1,15 @@ +//go:build !linux && !darwin && !windows && !freebsd && !openbsd && !netbsd && !wasm && !test_web_driver + +package app + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/theme" +) + +// DefaultVariant returns the systems default fyne.ThemeVariant. +// Normally, you should not need this. It is extracted out of the root app package to give the +// settings app access to it. +func DefaultVariant() fyne.ThemeVariant { + return theme.VariantDark +} diff --git a/vendor/fyne.io/fyne/v2/internal/app/theme_wasm.go b/vendor/fyne.io/fyne/v2/internal/app/theme_wasm.go new file mode 100644 index 0000000..ca00087 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/app/theme_wasm.go @@ -0,0 +1,24 @@ +//go:build wasm + +package app + +import ( + "syscall/js" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/theme" +) + +// DefaultVariant returns the systems default fyne.ThemeVariant. +// Normally, you should not need this. It is extracted out of the root app package to give the +// settings app access to it. +func DefaultVariant() fyne.ThemeVariant { + matches := js.Global().Call("matchMedia", "(prefers-color-scheme: dark)") + if matches.Truthy() { + if matches.Get("matches").Bool() { + return theme.VariantDark + } + return theme.VariantLight + } + return theme.VariantDark +} diff --git a/vendor/fyne.io/fyne/v2/internal/app/theme_web.go b/vendor/fyne.io/fyne/v2/internal/app/theme_web.go new file mode 100644 index 0000000..fcb8660 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/app/theme_web.go @@ -0,0 +1,15 @@ +//go:build !wasm && test_web_driver + +package app + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/theme" +) + +// DefaultVariant returns the systems default fyne.ThemeVariant. +// Normally, you should not need this. It is extracted out of the root app package to give the +// settings app access to it. +func DefaultVariant() fyne.ThemeVariant { + return theme.VariantDark +} diff --git a/vendor/fyne.io/fyne/v2/internal/app/theme_windows.go b/vendor/fyne.io/fyne/v2/internal/app/theme_windows.go new file mode 100644 index 0000000..13d4a9c --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/app/theme_windows.go @@ -0,0 +1,63 @@ +//go:build !android && !ios && !wasm && !test_web_driver + +package app + +import ( + "syscall" + + "golang.org/x/sys/windows/registry" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/theme" +) + +const themeRegKey = `SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize` + +// DefaultVariant returns the systems default fyne.ThemeVariant. +// Normally, you should not need this. It is extracted out of the root app package to give the +// settings app access to it. +func DefaultVariant() fyne.ThemeVariant { + if isDark() { + return theme.VariantDark + } + return theme.VariantLight +} + +func isDark() bool { + k, err := registry.OpenKey(registry.CURRENT_USER, themeRegKey, registry.QUERY_VALUE) + if err != nil { // older version of Windows will not have this key + return false + } + defer k.Close() + + useLight, _, err := k.GetIntegerValue("AppsUseLightTheme") + if err != nil { // older version of Windows will not have this value + return false + } + + return useLight == 0 +} + +// WatchTheme calls the supplied function when the Windows dark/light theme changes. +func WatchTheme(onChanged func()) { + // implementation based on an MIT-licensed Github Gist by Jeremy Black (c) 2022 + // https://gist.github.com/jerblack/1d05bbcebb50ad55c312e4d7cf1bc909 + var regNotifyChangeKeyValue *syscall.Proc + if advapi32, err := syscall.LoadDLL("Advapi32.dll"); err == nil { + if p, err := advapi32.FindProc("RegNotifyChangeKeyValue"); err == nil { + regNotifyChangeKeyValue = p + } + } + if regNotifyChangeKeyValue == nil { + return + } + k, err := registry.OpenKey(registry.CURRENT_USER, themeRegKey, syscall.KEY_NOTIFY|registry.QUERY_VALUE) + if err != nil { + return // on older versions of windows the key may not exist + } + for { + // blocks until the registry key has been changed + regNotifyChangeKeyValue.Call(uintptr(k), 0, 0x00000001|0x00000004, 0, 0) + onChanged() + } +} diff --git a/vendor/fyne.io/fyne/v2/internal/app/theme_xdg.go b/vendor/fyne.io/fyne/v2/internal/app/theme_xdg.go new file mode 100644 index 0000000..8763469 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/app/theme_xdg.go @@ -0,0 +1,20 @@ +//go:build !wasm && !test_web_driver && !android && !ios && !mobile && (linux || openbsd || freebsd || netbsd) + +package app + +import ( + "sync/atomic" + + "fyne.io/fyne/v2" +) + +// CurrentVariant contains the system’s theme variant. +// It is intended for internal use, only! +var CurrentVariant atomic.Uint64 + +// DefaultVariant returns the systems default fyne.ThemeVariant. +// Normally, you should not need this. It is extracted out of the root app package to give the +// settings app access to it. +func DefaultVariant() fyne.ThemeVariant { + return fyne.ThemeVariant(CurrentVariant.Load()) +} diff --git a/vendor/fyne.io/fyne/v2/internal/async/chan.go b/vendor/fyne.io/fyne/v2/internal/async/chan.go new file mode 100644 index 0000000..7afe18d --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/async/chan.go @@ -0,0 +1,97 @@ +package async + +// UnboundedChan is a channel with an unbounded buffer for caching +// Func objects. A channel must be closed via Close method. +type UnboundedChan[T any] struct { + in, out chan T + close chan struct{} + q []T +} + +// NewUnboundedChan returns a unbounded channel with unlimited capacity. +func NewUnboundedChan[T any]() *UnboundedChan[T] { + ch := &UnboundedChan[T]{ + // The size of Func, Interface, and CanvasObject are all less than 16 bytes, we use 16 to fit + // a CPU cache line (L2, 256 Bytes), which may reduce cache misses. + in: make(chan T, 16), + out: make(chan T, 16), + close: make(chan struct{}), + } + go ch.processing() + return ch +} + +// In returns the send channel of the given channel, which can be used to +// send values to the channel. +func (ch *UnboundedChan[T]) In() chan<- T { return ch.in } + +// Out returns the receive channel of the given channel, which can be used +// to receive values from the channel. +func (ch *UnboundedChan[T]) Out() <-chan T { return ch.out } + +// Close closes the channel. +func (ch *UnboundedChan[T]) Close() { ch.close <- struct{}{} } + +func (ch *UnboundedChan[T]) processing() { + // This is a preallocation of the internal unbounded buffer. + // The size is randomly picked. But if one changes the size, the + // reallocation size at the subsequent for loop should also be + // changed too. Furthermore, there is no memory leak since the + // queue is garbage collected. + ch.q = make([]T, 0, 1<<10) + for { + select { + case e, ok := <-ch.in: + if !ok { + // We don't want the input channel be accidentally closed + // via close() instead of Close(). If that happens, it is + // a misuse, do a panic as warning. + panic("async: misuse of unbounded channel, In() was closed") + } + ch.q = append(ch.q, e) + case <-ch.close: + ch.closed() + return + } + for len(ch.q) > 0 { + select { + case ch.out <- ch.q[0]: + ch.q[0] = *new(T) // de-reference earlier to help GC (use clear() when Go 1.21 is base) + ch.q = ch.q[1:] + case e, ok := <-ch.in: + if !ok { + // We don't want the input channel be accidentally closed + // via close() instead of Close(). If that happens, it is + // a misuse, do a panic as warning. + panic("async: misuse of unbounded channel, In() was closed") + } + ch.q = append(ch.q, e) + case <-ch.close: + ch.closed() + return + } + } + // If the remaining capacity is too small, we prefer to + // reallocate the entire buffer. + if cap(ch.q) < 1<<5 { + ch.q = make([]T, 0, 1<<10) + } + } +} + +func (ch *UnboundedChan[T]) closed() { + close(ch.in) + for e := range ch.in { + ch.q = append(ch.q, e) + } + for len(ch.q) > 0 { + select { + case ch.out <- ch.q[0]: + ch.q[0] = *new(T) // de-reference earlier to help GC (use clear() when Go 1.21 is base) + ch.q = ch.q[1:] + default: + } + } + close(ch.out) + close(ch.close) +} diff --git a/vendor/fyne.io/fyne/v2/internal/async/chan_struct.go b/vendor/fyne.io/fyne/v2/internal/async/chan_struct.go new file mode 100644 index 0000000..2ad3e6e --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/async/chan_struct.go @@ -0,0 +1,84 @@ +package async + +// UnboundedStructChan is a channel with an unbounded buffer for caching +// struct{} objects. This implementation is a specialized version that +// optimizes for struct{} objects than other types. A channel must be +// closed via Close method. +type UnboundedStructChan struct { + in, out, close chan struct{} + n uint64 +} + +// NewUnboundedStructChan returns a unbounded channel with unlimited capacity. +func NewUnboundedStructChan() *UnboundedStructChan { + ch := &UnboundedStructChan{ + // The size of Struct is less than 16 bytes, we use 16 to fit + // a CPU cache line (L2, 256 Bytes), which may reduce cache misses. + in: make(chan struct{}, 16), + out: make(chan struct{}, 16), + close: make(chan struct{}), + } + go ch.processing() + return ch +} + +// In returns a send-only channel that can be used to send values +// to the channel. +func (ch *UnboundedStructChan) In() chan<- struct{} { return ch.in } + +// Out returns a receive-only channel that can be used to receive +// values from the channel. +func (ch *UnboundedStructChan) Out() <-chan struct{} { return ch.out } + +// Close closes the channel. +func (ch *UnboundedStructChan) Close() { ch.close <- struct{}{} } + +func (ch *UnboundedStructChan) processing() { + for { + select { + case _, ok := <-ch.in: + if !ok { + // We don't want the input channel be accidentally closed + // via close() instead of Close(). If that happens, it is + // a misuse, do a panic as warning. + panic("async: misuse of unbounded channel, In() was closed") + } + ch.n++ + case <-ch.close: + ch.closed() + return + } + for ch.n > 0 { + select { + case ch.out <- struct{}{}: + ch.n-- + case _, ok := <-ch.in: + if !ok { + // We don't want the input channel be accidentally closed + // via close() instead of Close(). If that happens, it is + // a misuse, do a panic as warning. + panic("async: misuse of unbounded channel, In() was closed") + } + ch.n++ + case <-ch.close: + ch.closed() + return + } + } + } +} + +func (ch *UnboundedStructChan) closed() { + close(ch.in) + for range ch.in { + ch.n++ + } + for ; ch.n > 0; ch.n-- { + select { + case ch.out <- struct{}{}: + default: + } + } + close(ch.out) + close(ch.close) +} diff --git a/vendor/fyne.io/fyne/v2/internal/async/doc.go b/vendor/fyne.io/fyne/v2/internal/async/doc.go new file mode 100644 index 0000000..9ac614f --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/async/doc.go @@ -0,0 +1,18 @@ +// Package async provides unbounded channel and queue structures that are +// designed for caching unlimited number of a concrete type. For better +// performance, a given type should be less or euqal than 16 bytes. +// +// The difference of an unbounded channel or queue is that unbounde channels +// can utilize select and channel semantics, whereas queue cannot. A user of +// this package should balance this tradeoff. For instance, an unbounded +// channel can provide zero waiting cost when trying to receiving an object +// when the receiving select statement has a default case, and a queue can +// only receive the object with a time amount of time, but depending on the +// number of queue item producer, the receiving time may increase accordingly. +// +// Delicate dance: One must aware that an unbounded channel may lead to +// OOM when the consuming speed of the buffer is lower than the producing +// speed constantly. However, such a channel may be fairly used for event +// delivering if the consumer of the channel consumes the incoming +// forever, such as even processing. +package async diff --git a/vendor/fyne.io/fyne/v2/internal/async/goroutine.go b/vendor/fyne.io/fyne/v2/internal/async/goroutine.go new file mode 100644 index 0000000..7a75dc2 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/async/goroutine.go @@ -0,0 +1,85 @@ +package async + +import ( + "log" + "runtime" + "strings" + "sync/atomic" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/build" +) + +// mainGoroutineID stores the main goroutine ID. +// This ID must be initialized during setup by calling `SetMainGoroutine` because +// a main goroutine may not equal to 1 due to the influence of a garbage collector. +var mainGoroutineID atomic.Uint64 + +func SetMainGoroutine() { + mainGoroutineID.Store(goroutineID()) +} + +// EnsureNotMain is part of our thread transition and makes sure that the passed function runs off main. +// If the context is running on a goroutine or the transition has been disabled this will blindly run. +// Otherwise, an error will be logged and the function will be called on a new goroutine. +// +// This will be removed later and should never be public +func EnsureNotMain(fn func()) { + if build.MigratedToFyneDo() || !IsMainGoroutine() { + fn() + return + } + + log.Println("*** Error in Fyne call thread, fyne.Do[AndWait] called from main goroutine ***") + + logStackTop(2) + go fn() +} + +// EnsureMain is part of our thread transition and makes sure that the passed function runs on main. +// If the context is main or the transition has been disabled this will blindly run. +// Otherwise, an error will be logged and the function will be called on the main goroutine. +// +// This will be removed later and should never be public +func EnsureMain(fn func()) { + if build.MigratedToFyneDo() || IsMainGoroutine() { + fn() + return + } + + log.Println("*** Error in Fyne call thread, this should have been called in fyne.Do[AndWait] ***") + + logStackTop(1) + fyne.DoAndWait(fn) +} + +func logStackTop(skip int) { + pc := make([]uintptr, 16) + _ = runtime.Callers(skip, pc) + frames := runtime.CallersFrames(pc) + frame, more := frames.Next() + + var nextFrame runtime.Frame + for more { + nextFrame, more = frames.Next() + if nextFrame.File == "" || strings.Contains(nextFrame.File, "runtime") { // don't descend into Go + break + } + + frame = nextFrame + if !strings.Contains(nextFrame.File, "/fyne/") { // skip library lines + break + } + } + log.Printf(" From: %s:%d", frame.File, frame.Line) +} + +func goroutineID() (id uint64) { + var buf [30]byte + runtime.Stack(buf[:], false) + for i := 10; buf[i] != ' '; i++ { + id = id*10 + uint64(buf[i]&15) + } + + return id +} diff --git a/vendor/fyne.io/fyne/v2/internal/async/goroutine_desktop.go b/vendor/fyne.io/fyne/v2/internal/async/goroutine_desktop.go new file mode 100644 index 0000000..2dc1fa7 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/async/goroutine_desktop.go @@ -0,0 +1,8 @@ +//go:build !mobile + +package async + +// IsMainGoroutine returns true if it is called from the main goroutine, false otherwise. +func IsMainGoroutine() bool { + return goroutineID() == mainGoroutineID.Load() +} diff --git a/vendor/fyne.io/fyne/v2/internal/async/goroutine_mobile.go b/vendor/fyne.io/fyne/v2/internal/async/goroutine_mobile.go new file mode 100644 index 0000000..d2b74b2 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/async/goroutine_mobile.go @@ -0,0 +1,9 @@ +//go:build mobile + +package async + +// IsMainGoroutine returns true if it is called from the main goroutine, false otherwise. +func IsMainGoroutine() bool { + routineID := mainGoroutineID.Load() + return routineID == 0 || goroutineID() == routineID +} diff --git a/vendor/fyne.io/fyne/v2/internal/async/map.go b/vendor/fyne.io/fyne/v2/internal/async/map.go new file mode 100644 index 0000000..e3aa859 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/async/map.go @@ -0,0 +1,67 @@ +//go:build !migrated_fynedo + +package async + +import "sync" + +// Map is a generic wrapper around [sync.Map]. +type Map[K, V any] struct { + sync.Map +} + +// Delete deletes the value for a key. +func (m *Map[K, V]) Delete(key K) { + m.Map.Delete(key) +} + +// Len returns the length of the map. It is O(n) over the number of items. +func (m *Map[K, V]) Len() (count int) { + m.Map.Range(func(_, _ any) bool { + count++ + return true + }) + return count +} + +// Load returns the value stored in the map for a key, or nil if no value is present. +// The ok result indicates whether value was found in the map. +func (m *Map[K, V]) Load(key K) (value V, ok bool) { + val, ok := m.Map.Load(key) + if val == nil { + return *new(V), ok + } + return val.(V), ok +} + +// LoadAndDelete deletes the value for a key, returning the previous value if any. +// The loaded result reports whether the key was present. +func (m *Map[K, V]) LoadAndDelete(key K) (value V, loaded bool) { + val, loaded := m.Map.LoadAndDelete(key) + if val == nil { + return *new(V), loaded + } + return val.(V), loaded +} + +// LoadOrStore returns the existing value for the key if present. +// Otherwise, it stores and returns the given value. +// The loaded result is true if the value was loaded, false if stored. +func (m *Map[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) { + act, loaded := m.Map.LoadOrStore(key, value) + if act == nil { + return *new(V), loaded + } + return act.(V), loaded +} + +// Range calls f sequentially for each key and value present in the map. If f returns false, range stops the iteration. +func (m *Map[K, V]) Range(f func(key K, value V) bool) { + m.Map.Range(func(key, value any) bool { + return f(key.(K), value.(V)) + }) +} + +// Store sets the value for a key. +func (m *Map[K, V]) Store(key K, value V) { + m.Map.Store(key, value) +} diff --git a/vendor/fyne.io/fyne/v2/internal/async/map_clear.go b/vendor/fyne.io/fyne/v2/internal/async/map_clear.go new file mode 100644 index 0000000..d365103 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/async/map_clear.go @@ -0,0 +1,12 @@ +//go:build !go1.23 && !migrated_fynedo + +package async + +// Clear deletes all the entries, resulting in an empty Map. +// This is O(n) over the number of entries when not using Go 1.23 or newer. +func (m *Map[K, V]) Clear() { + m.Map.Range(func(key, _ any) bool { + m.Map.Delete(key) + return true + }) +} diff --git a/vendor/fyne.io/fyne/v2/internal/async/map_clear_go1.23.go b/vendor/fyne.io/fyne/v2/internal/async/map_clear_go1.23.go new file mode 100644 index 0000000..c3ea5d6 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/async/map_clear_go1.23.go @@ -0,0 +1,8 @@ +//go:build go1.23 && !migrated_fynedo + +package async + +// Clear deletes all the entries, resulting in an empty Map. +func (m *Map[K, V]) Clear() { + m.Map.Clear() // More efficient than O(n) range and delete in older Go. +} diff --git a/vendor/fyne.io/fyne/v2/internal/async/map_migratedfynedo.go b/vendor/fyne.io/fyne/v2/internal/async/map_migratedfynedo.go new file mode 100644 index 0000000..bd7fa39 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/async/map_migratedfynedo.go @@ -0,0 +1,72 @@ +//go:build migrated_fynedo + +package async + +// Map is a generic wrapper around [sync.Map]. +type Map[K any, V any] struct { + // Use "comparable" as type constraint and map[K]V as the inner type + // once Go 1.20 is our minimum version so interfaces can be used as keys. + m map[any]V +} + +// Delete deletes the value for a key. +func (m *Map[K, V]) Delete(key K) { + delete(m.m, key) +} + +// Len returns the length of the map. It is O(n) over the number of items. +func (m *Map[K, V]) Len() (count int) { + return len(m.m) +} + +// Load returns the value stored in the map for a key, or nil if no value is present. +// The ok result indicates whether value was found in the map. +func (m *Map[K, V]) Load(key K) (value V, ok bool) { + val, ok := m.m[key] + return val, ok +} + +// LoadAndDelete deletes the value for a key, returning the previous value if any. +// The loaded result reports whether the key was present. +func (m *Map[K, V]) LoadAndDelete(key K) (value V, loaded bool) { + val, ok := m.m[key] + delete(m.m, key) + return val, ok +} + +// LoadOrStore returns the existing value for the key if present. +// Otherwise, it stores and returns the given value. +// The loaded result is true if the value was loaded, false if stored. +func (m *Map[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) { + if m.m == nil { + m.m = make(map[any]V) + } + + if val, ok := m.m[key]; ok { + return val, true + } + m.m[key] = value + return value, false +} + +// Range calls f sequentially for each key and value present in the map. If f returns false, range stops the iteration. +func (m *Map[K, V]) Range(f func(key K, value V) bool) { + for k, v := range m.m { + if !f(k.(K), v) { + return + } + } +} + +// Store sets the value for a key. +func (m *Map[K, V]) Store(key K, value V) { + if m.m == nil { + m.m = make(map[any]V) + } + m.m[key] = value +} + +// Clear removes all entries from the map. +func (m *Map[K, V]) Clear() { + m.m = make(map[any]V) // Use range-and-delete loop once Go 1.20 is the minimum version. +} diff --git a/vendor/fyne.io/fyne/v2/internal/async/pool.go b/vendor/fyne.io/fyne/v2/internal/async/pool.go new file mode 100644 index 0000000..a65f748 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/async/pool.go @@ -0,0 +1,30 @@ +package async + +import "sync" + +// Implementation inspired by https://github.com/tailscale/tailscale/blob/main/syncs/pool.go. + +// Pool is the generic version of sync.Pool. +type Pool[T any] struct { + pool sync.Pool + + // New specifies a function to generate + // a value when Get would otherwise return the zero value of T. + New func() T +} + +// Get selects an arbitrary item from the Pool, removes it from the Pool, +// and returns it to the caller. +func (p *Pool[T]) Get() T { + x, ok := p.pool.Get().(T) + if !ok && p.New != nil { + return p.New() + } + + return x +} + +// Put adds x to the pool. +func (p *Pool[T]) Put(x T) { + p.pool.Put(x) +} diff --git a/vendor/fyne.io/fyne/v2/internal/async/queue.go b/vendor/fyne.io/fyne/v2/internal/async/queue.go new file mode 100644 index 0000000..71681ea --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/async/queue.go @@ -0,0 +1,92 @@ +//go:build !migrated_fynedo + +package async + +import ( + "sync/atomic" + + "fyne.io/fyne/v2" +) + +// CanvasObjectQueue implements lock-free FIFO freelist based queue. +// +// Reference: https://dl.acm.org/citation.cfm?doid=248052.248106 +type CanvasObjectQueue struct { + head atomic.Pointer[itemCanvasObject] + tail atomic.Pointer[itemCanvasObject] + len atomic.Uint64 +} + +// NewCanvasObjectQueue returns a queue for caching values. +func NewCanvasObjectQueue() *CanvasObjectQueue { + head := &itemCanvasObject{} + queue := &CanvasObjectQueue{} + queue.head.Store(head) + queue.tail.Store(head) + return queue +} + +type itemCanvasObject struct { + next atomic.Pointer[itemCanvasObject] + v fyne.CanvasObject +} + +var itemCanvasObjectPool = Pool[*itemCanvasObject]{ + New: func() *itemCanvasObject { return &itemCanvasObject{} }, +} + +// In puts the given value at the tail of the queue. +func (q *CanvasObjectQueue) In(v fyne.CanvasObject) { + i := itemCanvasObjectPool.Get() + i.next.Store(nil) + i.v = v + + var last, lastnext *itemCanvasObject + for { + last = q.tail.Load() + lastnext = last.next.Load() + if q.tail.Load() == last { + if lastnext == nil { + if last.next.CompareAndSwap(lastnext, i) { + q.tail.CompareAndSwap(last, i) + q.len.Add(1) + return + } + } else { + q.tail.CompareAndSwap(last, lastnext) + } + } + } +} + +// Out removes and returns the value at the head of the queue. +// It returns nil if the queue is empty. +func (q *CanvasObjectQueue) Out() fyne.CanvasObject { + var first, last, firstnext *itemCanvasObject + for { + first = q.head.Load() + last = q.tail.Load() + firstnext = first.next.Load() + if first == q.head.Load() { + if first == last { + if firstnext == nil { + return nil + } + + q.tail.CompareAndSwap(last, firstnext) + } else { + v := firstnext.v + if q.head.CompareAndSwap(first, firstnext) { + q.len.Add(^uint64(0)) + itemCanvasObjectPool.Put(first) + return v + } + } + } + } +} + +// Len returns the length of the queue. +func (q *CanvasObjectQueue) Len() uint64 { + return q.len.Load() +} diff --git a/vendor/fyne.io/fyne/v2/internal/async/queue_migratedfynedo.go b/vendor/fyne.io/fyne/v2/internal/async/queue_migratedfynedo.go new file mode 100644 index 0000000..2f12a86 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/async/queue_migratedfynedo.go @@ -0,0 +1,60 @@ +//go:build migrated_fynedo + +package async + +import "fyne.io/fyne/v2" + +const defaultQueueCapacity = 64 + +// CanvasObjectQueue represents a single-threaded queue for managing canvas objects using a ring buffer. +type CanvasObjectQueue struct { + buffer []fyne.CanvasObject + head int + size int +} + +// NewCanvasObjectQueue returns a queue for caching values with an initial capacity. +func NewCanvasObjectQueue() *CanvasObjectQueue { + return &CanvasObjectQueue{buffer: make([]fyne.CanvasObject, defaultQueueCapacity)} +} + +// In adds the given value to the tail of the queue. +// If the queue is full, it grows the buffer dynamically. +func (q *CanvasObjectQueue) In(v fyne.CanvasObject) { + if q.size == len(q.buffer) { + buffer := make([]fyne.CanvasObject, len(q.buffer)*2) + copy(buffer, q.buffer[q.head:]) + copy(buffer[len(q.buffer)-q.head:], q.buffer[:q.head]) + q.buffer = buffer + q.head = 0 + } + + tail := (q.head + q.size) % len(q.buffer) + q.buffer[tail] = v + q.size++ +} + +// Out removes and returns the value at the head of the queue. +// It returns nil if the queue is empty. +func (q *CanvasObjectQueue) Out() fyne.CanvasObject { + if q.size == 0 { + return nil + } + + first := q.buffer[q.head] + q.buffer[q.head] = nil + q.head = (q.head + 1) % len(q.buffer) + q.size-- + + if q.size == 0 && len(q.buffer) > 4*defaultQueueCapacity { + q.buffer = make([]fyne.CanvasObject, defaultQueueCapacity) + q.head = 0 + } + + return first +} + +// Len returns the number of items in the queue. +func (q *CanvasObjectQueue) Len() uint64 { + return uint64(q.size) +} diff --git a/vendor/fyne.io/fyne/v2/internal/build/animations_disabled.go b/vendor/fyne.io/fyne/v2/internal/build/animations_disabled.go new file mode 100644 index 0000000..4d5a83d --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/build/animations_disabled.go @@ -0,0 +1,7 @@ +//go:build no_animations + +package build + +// NoAnimations is true if the application was built without animations by +// passing the no_animations build tag. +const NoAnimations = true diff --git a/vendor/fyne.io/fyne/v2/internal/build/animations_enabled.go b/vendor/fyne.io/fyne/v2/internal/build/animations_enabled.go new file mode 100644 index 0000000..e1eba8f --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/build/animations_enabled.go @@ -0,0 +1,7 @@ +//go:build !no_animations + +package build + +// NoAnimations is true if the application was built without animations by +// passing the no_animations build tag. +const NoAnimations = false diff --git a/vendor/fyne.io/fyne/v2/internal/build/build.go b/vendor/fyne.io/fyne/v2/internal/build/build.go new file mode 100644 index 0000000..8567284 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/build/build.go @@ -0,0 +1,29 @@ +// Package build contains information about they type of build currently running. +package build + +import ( + "sync" + + "fyne.io/fyne/v2" +) + +var ( + migrateCheck sync.Once + + migratedFyneDo bool +) + +func MigratedToFyneDo() bool { + if DisableThreadChecks { + return true + } + + migrateCheck.Do(func() { + v, ok := fyne.CurrentApp().Metadata().Migrations["fyneDo"] + if ok { + migratedFyneDo = v + } + }) + + return migratedFyneDo +} diff --git a/vendor/fyne.io/fyne/v2/internal/build/driver_flatpak.go b/vendor/fyne.io/fyne/v2/internal/build/driver_flatpak.go new file mode 100644 index 0000000..0261f7b --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/build/driver_flatpak.go @@ -0,0 +1,6 @@ +//go:build flatpak + +package build + +// IsFlatpak is true if the binary is compiled for a Flatpak package. +const IsFlatpak = true diff --git a/vendor/fyne.io/fyne/v2/internal/build/driver_notflatpak.go b/vendor/fyne.io/fyne/v2/internal/build/driver_notflatpak.go new file mode 100644 index 0000000..2f891ba --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/build/driver_notflatpak.go @@ -0,0 +1,6 @@ +//go:build !flatpak + +package build + +// IsFlatpak is true if the binary is compiled for a Flatpak package. +const IsFlatpak = false diff --git a/vendor/fyne.io/fyne/v2/internal/build/driver_notwayland.go b/vendor/fyne.io/fyne/v2/internal/build/driver_notwayland.go new file mode 100644 index 0000000..ba398c2 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/build/driver_notwayland.go @@ -0,0 +1,6 @@ +//go:build !wayland + +package build + +// IsWayland is true when compiling for the wayland windowing system. +const IsWayland = false diff --git a/vendor/fyne.io/fyne/v2/internal/build/driver_wayland.go b/vendor/fyne.io/fyne/v2/internal/build/driver_wayland.go new file mode 100644 index 0000000..d48bb51 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/build/driver_wayland.go @@ -0,0 +1,6 @@ +//go:build wayland + +package build + +// IsWayland is true when compiling for the wayland windowing system. +const IsWayland = true diff --git a/vendor/fyne.io/fyne/v2/internal/build/hints_disabled.go b/vendor/fyne.io/fyne/v2/internal/build/hints_disabled.go new file mode 100644 index 0000000..f8919d0 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/build/hints_disabled.go @@ -0,0 +1,7 @@ +//go:build !hints + +package build + +// HasHints is false to indicate that hints are not currently switched on. +// To enable please rebuild with "-tags hints" parameters. +const HasHints = false diff --git a/vendor/fyne.io/fyne/v2/internal/build/hints_enabled.go b/vendor/fyne.io/fyne/v2/internal/build/hints_enabled.go new file mode 100644 index 0000000..db627a3 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/build/hints_enabled.go @@ -0,0 +1,6 @@ +//go:build hints + +package build + +// HasHints is true to indicate that hints are currently switched on. +const HasHints = true diff --git a/vendor/fyne.io/fyne/v2/internal/build/menu_integrated.go b/vendor/fyne.io/fyne/v2/internal/build/menu_integrated.go new file mode 100644 index 0000000..2b5365f --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/build/menu_integrated.go @@ -0,0 +1,6 @@ +//go:build !darwin || no_native_menus + +package build + +// HasNativeMenu is true if the app is built with support for native menu. +const HasNativeMenu = false diff --git a/vendor/fyne.io/fyne/v2/internal/build/menu_native.go b/vendor/fyne.io/fyne/v2/internal/build/menu_native.go new file mode 100644 index 0000000..7181e52 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/build/menu_native.go @@ -0,0 +1,6 @@ +//go:build darwin && !no_native_menus + +package build + +// HasNativeMenu is true if the app is built with support for native menu. +const HasNativeMenu = true diff --git a/vendor/fyne.io/fyne/v2/internal/build/metadata.go b/vendor/fyne.io/fyne/v2/internal/build/metadata.go new file mode 100644 index 0000000..fa525f9 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/build/metadata.go @@ -0,0 +1,6 @@ +//go:build !no_metadata + +package build + +// NoMetadata is false if the compiler flags have not turned off metadata support. +const NoMetadata = false diff --git a/vendor/fyne.io/fyne/v2/internal/build/metadata_disabled.go b/vendor/fyne.io/fyne/v2/internal/build/metadata_disabled.go new file mode 100644 index 0000000..6217e15 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/build/metadata_disabled.go @@ -0,0 +1,6 @@ +//go:build no_metadata + +package build + +// NoMetadata is true if the compiler flags have turned off metadata support. +const NoMetadata = true diff --git a/vendor/fyne.io/fyne/v2/internal/build/migrated_fynedo.go b/vendor/fyne.io/fyne/v2/internal/build/migrated_fynedo.go new file mode 100644 index 0000000..089f766 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/build/migrated_fynedo.go @@ -0,0 +1,6 @@ +//go:build migrated_fynedo + +package build + +// DisableThreadChecks disables the thread safety checks for performance. +const DisableThreadChecks = true diff --git a/vendor/fyne.io/fyne/v2/internal/build/migrated_notfynedo.go b/vendor/fyne.io/fyne/v2/internal/build/migrated_notfynedo.go new file mode 100644 index 0000000..a9fbdd5 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/build/migrated_notfynedo.go @@ -0,0 +1,6 @@ +//go:build !migrated_fynedo + +package build + +// DisableThreadChecks set to false enables the thread safety checks for logging of incorrect usage. +const DisableThreadChecks = false diff --git a/vendor/fyne.io/fyne/v2/internal/build/mode_debug.go b/vendor/fyne.io/fyne/v2/internal/build/mode_debug.go new file mode 100644 index 0000000..9658ddc --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/build/mode_debug.go @@ -0,0 +1,8 @@ +//go:build debug + +package build + +import "fyne.io/fyne/v2" + +// Mode is the application's build mode. +const Mode = fyne.BuildDebug diff --git a/vendor/fyne.io/fyne/v2/internal/build/mode_release.go b/vendor/fyne.io/fyne/v2/internal/build/mode_release.go new file mode 100644 index 0000000..3b6937e --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/build/mode_release.go @@ -0,0 +1,8 @@ +//go:build release + +package build + +import "fyne.io/fyne/v2" + +// Mode is the application's build mode. +const Mode = fyne.BuildRelease diff --git a/vendor/fyne.io/fyne/v2/internal/build/mode_standard.go b/vendor/fyne.io/fyne/v2/internal/build/mode_standard.go new file mode 100644 index 0000000..7565646 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/build/mode_standard.go @@ -0,0 +1,8 @@ +//go:build !debug && !release + +package build + +import "fyne.io/fyne/v2" + +// Mode is the application's build mode. +const Mode = fyne.BuildStandard diff --git a/vendor/fyne.io/fyne/v2/internal/cache/base.go b/vendor/fyne.io/fyne/v2/internal/cache/base.go new file mode 100644 index 0000000..0946566 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/cache/base.go @@ -0,0 +1,122 @@ +package cache + +import ( + "os" + "time" + + "fyne.io/fyne/v2" +) + +var ( + ValidDuration = 1 * time.Minute + cleanTaskInterval = ValidDuration / 2 + + lastClean time.Time + skippedCleanWithCanvasRefresh = false + + // testing purpose only + timeNow = time.Now +) + +func init() { + if t, err := time.ParseDuration(os.Getenv("FYNE_CACHE")); err == nil { + ValidDuration = t + cleanTaskInterval = ValidDuration / 2 + } +} + +// Clean run cache clean task, it should be called on paint events. +func Clean(canvasRefreshed bool) { + now := timeNow() + // do not run clean task too fast + if now.Sub(lastClean) < 10*time.Second { + if canvasRefreshed { + skippedCleanWithCanvasRefresh = true + } + return + } + if skippedCleanWithCanvasRefresh { + skippedCleanWithCanvasRefresh = false + canvasRefreshed = true + } + if !canvasRefreshed && now.Sub(lastClean) < cleanTaskInterval { + return + } + destroyExpiredSvgs(now) + destroyExpiredFontMetrics(now) + if canvasRefreshed { + // Destroy renderers on canvas refresh to avoid flickering screen. + destroyExpiredRenderers(now) + // canvases cache should be invalidated only on canvas refresh, otherwise there wouldn't + // be a way to recover them later + destroyExpiredCanvases(now) + } + lastClean = timeNow() +} + +// CleanCanvas performs a complete remove of all the objects that belong to the specified +// canvas. Usually used to free all objects from a closing windows. +func CleanCanvas(canvas fyne.Canvas) { + canvases.Range(func(obj fyne.CanvasObject, cinfo *canvasInfo) bool { + if cinfo.canvas != canvas { + return true + } + + canvases.Delete(obj) + + wid, ok := obj.(fyne.Widget) + if !ok { + return true + } + rinfo, ok := renderers.LoadAndDelete(wid) + if !ok { + return true + } + rinfo.renderer.Destroy() + overrides.Delete(wid) + return true + }) +} + +// ResetThemeCaches clears all the svg and text size cache maps +func ResetThemeCaches() { + svgs.Clear() + fontSizeCache.Clear() +} + +// destroyExpiredCanvases deletes objects from the canvases cache. +func destroyExpiredCanvases(now time.Time) { + canvases.Range(func(obj fyne.CanvasObject, cinfo *canvasInfo) bool { + if cinfo.isExpired(now) { + canvases.Delete(obj) + } + return true + }) +} + +// destroyExpiredRenderers deletes the renderer from the cache and calls +// renderer.Destroy() +func destroyExpiredRenderers(now time.Time) { + renderers.Range(func(wid fyne.Widget, rinfo *rendererInfo) bool { + if rinfo.isExpired(now) { + rinfo.renderer.Destroy() + overrides.Delete(wid) + renderers.Delete(wid) + } + return true + }) +} + +type expiringCache struct { + expires time.Time +} + +// isExpired check if the cache data is expired. +func (c *expiringCache) isExpired(now time.Time) bool { + return c.expires.Before(now) +} + +// setAlive updates expiration time. +func (c *expiringCache) setAlive() { + c.expires = timeNow().Add(ValidDuration) +} diff --git a/vendor/fyne.io/fyne/v2/internal/cache/canvases.go b/vendor/fyne.io/fyne/v2/internal/cache/canvases.go new file mode 100644 index 0000000..61514e1 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/cache/canvases.go @@ -0,0 +1,35 @@ +package cache + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/async" +) + +var canvases async.Map[fyne.CanvasObject, *canvasInfo] + +// GetCanvasForObject returns the canvas for the specified object. +func GetCanvasForObject(obj fyne.CanvasObject) fyne.Canvas { + cinfo, ok := canvases.Load(obj) + if cinfo == nil || !ok { + return nil + } + cinfo.setAlive() + return cinfo.canvas +} + +// SetCanvasForObject sets the canvas for the specified object. +// The passed function will be called if the item was not previously attached to this canvas +func SetCanvasForObject(obj fyne.CanvasObject, c fyne.Canvas, setup func()) { + cinfo := &canvasInfo{canvas: c} + cinfo.setAlive() + + old, found := canvases.LoadOrStore(obj, cinfo) + if (!found || old.canvas != c) && setup != nil { + setup() + } +} + +type canvasInfo struct { + expiringCache + canvas fyne.Canvas +} diff --git a/vendor/fyne.io/fyne/v2/internal/cache/svg.go b/vendor/fyne.io/fyne/v2/internal/cache/svg.go new file mode 100644 index 0000000..74a4e22 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/cache/svg.go @@ -0,0 +1,63 @@ +package cache + +import ( + "image" + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/async" +) + +var svgs async.Map[string, *svgInfo] + +// GetSvg gets svg image from cache if it exists. +func GetSvg(name string, o fyne.CanvasObject, w int, h int) *image.NRGBA { + svginfo, ok := svgs.Load(overriddenName(name, o)) + if !ok || svginfo == nil { + return nil + } + + if svginfo.w != w || svginfo.h != h { + return nil + } + + svginfo.setAlive() + return svginfo.pix +} + +// SetSvg sets a svg into the cache map. +func SetSvg(name string, o fyne.CanvasObject, pix *image.NRGBA, w int, h int) { + sinfo := &svgInfo{ + pix: pix, + w: w, + h: h, + } + sinfo.setAlive() + svgs.Store(overriddenName(name, o), sinfo) +} + +type svgInfo struct { + expiringCache + pix *image.NRGBA + w, h int +} + +// destroyExpiredSvgs destroys expired svgs cache data. +func destroyExpiredSvgs(now time.Time) { + svgs.Range(func(key string, sinfo *svgInfo) bool { + if sinfo.isExpired(now) { + svgs.Delete(key) + } + return true + }) +} + +func overriddenName(name string, o fyne.CanvasObject) string { + if o != nil { // for overridden themes get the cache key right + if over, ok := overrides.Load(o); ok { + return over.cacheID + name + } + } + + return name +} diff --git a/vendor/fyne.io/fyne/v2/internal/cache/text.go b/vendor/fyne.io/fyne/v2/internal/cache/text.go new file mode 100644 index 0000000..d9eec25 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/cache/text.go @@ -0,0 +1,68 @@ +package cache + +import ( + "image/color" + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/async" +) + +var fontSizeCache async.Map[fontSizeEntry, *fontMetric] + +type fontMetric struct { + expiringCache + size fyne.Size + baseLine float32 +} + +type fontSizeEntry struct { + Text string + Size float32 + Style fyne.TextStyle + Source string +} + +type FontCacheEntry struct { + fontSizeEntry + + Canvas fyne.Canvas + Color color.Color +} + +// GetFontMetrics looks up a calculated size and baseline required for the specified text parameters. +func GetFontMetrics(text string, fontSize float32, style fyne.TextStyle, source fyne.Resource) (size fyne.Size, base float32) { + name := "" + if source != nil { + name = source.Name() + } + ent := fontSizeEntry{text, fontSize, style, name} + ret, ok := fontSizeCache.Load(ent) + if !ok { + return fyne.Size{Width: 0, Height: 0}, 0 + } + ret.setAlive() + return ret.size, ret.baseLine +} + +// SetFontMetrics stores a calculated font size and baseline for parameters that were missing from the cache. +func SetFontMetrics(text string, fontSize float32, style fyne.TextStyle, source fyne.Resource, size fyne.Size, base float32) { + name := "" + if source != nil { + name = source.Name() + } + ent := fontSizeEntry{text, fontSize, style, name} + metric := &fontMetric{size: size, baseLine: base} + metric.setAlive() + fontSizeCache.Store(ent, metric) +} + +// destroyExpiredFontMetrics destroys expired fontSizeCache entries +func destroyExpiredFontMetrics(now time.Time) { + fontSizeCache.Range(func(k fontSizeEntry, v *fontMetric) bool { + if v.isExpired(now) { + fontSizeCache.Delete(k) + } + return true + }) +} diff --git a/vendor/fyne.io/fyne/v2/internal/cache/texture_common.go b/vendor/fyne.io/fyne/v2/internal/cache/texture_common.go new file mode 100644 index 0000000..59672e8 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/cache/texture_common.go @@ -0,0 +1,111 @@ +package cache + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/async" +) + +var ( + textTextures async.Map[FontCacheEntry, *textureInfo] + objectTextures async.Map[fyne.CanvasObject, *textureInfo] +) + +// DeleteTexture deletes the texture from the cache map. +func DeleteTexture(obj fyne.CanvasObject) { + objectTextures.Delete(obj) +} + +// GetTextTexture gets cached texture for a text run. +func GetTextTexture(ent FontCacheEntry) (TextureType, bool) { + texInfo, ok := textTextures.Load(ent) + if texInfo == nil || !ok { + return NoTexture, false + } + texInfo.setAlive() + return texInfo.texture, true +} + +// GetTexture gets cached texture. +func GetTexture(obj fyne.CanvasObject) (TextureType, bool) { + texInfo, ok := objectTextures.Load(obj) + if texInfo == nil || !ok { + return NoTexture, false + } + texInfo.setAlive() + return texInfo.texture, true +} + +// RangeExpiredTexturesFor range over the expired textures for the specified canvas. +// +// Note: If this is used to free textures, then it should be called inside a current +// gl context to ensure textures are deleted from gl. +func RangeExpiredTexturesFor(canvas fyne.Canvas, f func(fyne.CanvasObject)) { + now := timeNow() + + textTextures.Range(func(key FontCacheEntry, tinfo *textureInfo) bool { + // Just free text directly when that string/style combo is done. + if tinfo.isExpired(now) && tinfo.canvas == canvas { + textTextures.Delete(key) + tinfo.textFree() + } + return true + }) + + objectTextures.Range(func(obj fyne.CanvasObject, tinfo *textureInfo) bool { + if tinfo.isExpired(now) && tinfo.canvas == canvas { + f(obj) + } + return true + }) +} + +// RangeTexturesFor range over the textures for the specified canvas. +// It will not return the texture for a `canvas.Text` as their render lifecycle is handled separately. +// +// Note: If this is used to free textures, then it should be called inside a current +// gl context to ensure textures are deleted from gl. +func RangeTexturesFor(canvas fyne.Canvas, f func(fyne.CanvasObject)) { + // Do nothing for texture cache, it lives outside the scope of an object. + objectTextures.Range(func(obj fyne.CanvasObject, tinfo *textureInfo) bool { + if tinfo.canvas == canvas { + f(obj) + } + return true + }) +} + +// DeleteTextTexturesFor deletes all text textures for the given canvas. +func DeleteTextTexturesFor(canvas fyne.Canvas) { + textTextures.Range(func(key FontCacheEntry, tinfo *textureInfo) bool { + if tinfo.canvas == canvas { + textTextures.Delete(key) + tinfo.textFree() + } + return true + }) +} + +// SetTextTexture sets cached texture for a text run. +func SetTextTexture(ent FontCacheEntry, texture TextureType, canvas fyne.Canvas, free func()) { + tinfo := prepareTexture(texture, canvas, free) + textTextures.Store(ent, tinfo) +} + +// SetTexture sets cached texture. +func SetTexture(obj fyne.CanvasObject, texture TextureType, canvas fyne.Canvas) { + tinfo := prepareTexture(texture, canvas, nil) + objectTextures.Store(obj, tinfo) +} + +func prepareTexture(texture TextureType, canvas fyne.Canvas, free func()) *textureInfo { + tinfo := &textureInfo{texture: texture, textFree: free} + tinfo.canvas = canvas + tinfo.setAlive() + return tinfo +} + +// textureCacheBase defines base texture cache object. +type textureCacheBase struct { + expiringCache + canvas fyne.Canvas +} diff --git a/vendor/fyne.io/fyne/v2/internal/cache/texture_desktop.go b/vendor/fyne.io/fyne/v2/internal/cache/texture_desktop.go new file mode 100644 index 0000000..9627731 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/cache/texture_desktop.go @@ -0,0 +1,21 @@ +//go:build !android && !ios && !mobile && !wasm && !test_web_driver + +package cache + +// TextureType represents an uploaded GL texture +type TextureType = uint32 + +// NoTexture used when there is no valid texture +var NoTexture = TextureType(0) + +type textureInfo struct { + textureCacheBase + + texture TextureType + textFree func() +} + +// IsValid will return true if the passed texture is potentially a texture +func IsValid(texture TextureType) bool { + return texture != NoTexture +} diff --git a/vendor/fyne.io/fyne/v2/internal/cache/texture_gomobile.go b/vendor/fyne.io/fyne/v2/internal/cache/texture_gomobile.go new file mode 100644 index 0000000..14ea8cc --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/cache/texture_gomobile.go @@ -0,0 +1,22 @@ +//go:build android || ios || mobile + +package cache + +import "fyne.io/fyne/v2/internal/driver/mobile/gl" + +// TextureType represents an uploaded GL texture +type TextureType = gl.Texture + +var NoTexture = gl.Texture{0} + +type textureInfo struct { + textureCacheBase + + texture TextureType + textFree func() +} + +// IsValid will return true if the passed texture is potentially a texture +func IsValid(texture TextureType) bool { + return texture != NoTexture +} diff --git a/vendor/fyne.io/fyne/v2/internal/cache/texture_wasm.go b/vendor/fyne.io/fyne/v2/internal/cache/texture_wasm.go new file mode 100644 index 0000000..246f902 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/cache/texture_wasm.go @@ -0,0 +1,22 @@ +//go:build wasm || test_web_driver + +package cache + +import "github.com/fyne-io/gl-js" + +// TextureType represents an uploaded GL texture +type TextureType = gl.Texture + +var NoTexture = gl.NoTexture + +type textureInfo struct { + textureCacheBase + + texture TextureType + textFree func() +} + +// IsValid will return true if the passed texture is potentially a texture +func IsValid(texture TextureType) bool { + return gl.Texture(texture).IsValid() +} diff --git a/vendor/fyne.io/fyne/v2/internal/cache/theme.go b/vendor/fyne.io/fyne/v2/internal/cache/theme.go new file mode 100644 index 0000000..f199843 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/cache/theme.go @@ -0,0 +1,93 @@ +package cache + +import ( + "strconv" + "sync/atomic" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/async" +) + +var ( + overrides async.Map[fyne.CanvasObject, *overrideScope] + overrideCount atomic.Uint32 +) + +type overrideScope struct { + th fyne.Theme + cacheID string +} + +// OverrideTheme allows an app to specify that a single object should use a different theme to the app. +// This should be used sparingly to avoid a jarring user experience. +// If the object is a container it will theme the children, if it is a canvas primitive it will do nothing. +// +// Since: 2.5 +func OverrideTheme(o fyne.CanvasObject, th fyne.Theme) { + id := overrideCount.Add(1) + s := &overrideScope{th: th, cacheID: strconv.Itoa(int(id))} + overrideTheme(o, s) +} + +func OverrideThemeMatchingScope(o, parent fyne.CanvasObject) bool { + scope, ok := overrides.Load(parent) + if !ok { // not overridden in parent + return false + } + + overrideTheme(o, scope) + return true +} + +func WidgetScopeID(o fyne.CanvasObject) string { + scope, ok := overrides.Load(o) + if !ok { + return "" + } + + return scope.cacheID +} + +func WidgetTheme(o fyne.CanvasObject) fyne.Theme { + scope, ok := overrides.Load(o) + if !ok { + return nil + } + + return scope.th +} + +func overrideContainer(c *fyne.Container, s *overrideScope) { + for _, o := range c.Objects { + overrideTheme(o, s) + } +} + +func overrideTheme(o fyne.CanvasObject, s *overrideScope) { + if _, ok := o.(interface{ SetDeviceIsMobile(bool) }); ok { // ThemeOverride without the import loop + return // do not apply this theme over a new scope + } + + switch c := o.(type) { + case fyne.Widget: + overrideWidget(c, s) + case *fyne.Container: + overrideContainer(c, s) + default: + overrides.Store(c, s) + } +} + +func overrideWidget(w fyne.Widget, s *overrideScope) { + ResetThemeCaches() + overrides.Store(w, s) + + r := Renderer(w) + if r == nil { + return + } + + for _, o := range r.Objects() { + overrideTheme(o, s) + } +} diff --git a/vendor/fyne.io/fyne/v2/internal/cache/widget.go b/vendor/fyne.io/fyne/v2/internal/cache/widget.go new file mode 100644 index 0000000..c3eee5a --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/cache/widget.go @@ -0,0 +1,74 @@ +package cache + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/async" +) + +var renderers async.Map[fyne.Widget, *rendererInfo] + +type isBaseWidget interface { + ExtendBaseWidget(fyne.Widget) + super() fyne.Widget +} + +// Renderer looks up the render implementation for a widget +// If one does not exist, it creates and caches a renderer for the widget. +func Renderer(wid fyne.Widget) fyne.WidgetRenderer { + renderer, ok := CachedRenderer(wid) + if !ok && wid != nil { + renderer = wid.CreateRenderer() + rinfo := &rendererInfo{renderer: renderer} + rinfo.setAlive() + renderers.Store(wid, rinfo) + } + + return renderer +} + +// CachedRenderer looks up the cached render implementation for a widget +// If a renderer does not exist in the cache, it returns nil, false. +func CachedRenderer(wid fyne.Widget) (fyne.WidgetRenderer, bool) { + if wid == nil { + return nil, false + } + + if wd, ok := wid.(isBaseWidget); ok { + if wd.super() != nil { + wid = wd.super() + } + } + + rinfo, ok := renderers.Load(wid) + if !ok { + return nil, false + } + + rinfo.setAlive() + return rinfo.renderer, true +} + +// DestroyRenderer frees a render implementation for a widget. +// This is typically for internal use only. +func DestroyRenderer(wid fyne.Widget) { + rinfo, ok := renderers.LoadAndDelete(wid) + if !ok { + return + } + if rinfo != nil { + rinfo.renderer.Destroy() + } + overrides.Delete(wid) +} + +// IsRendered returns true of the widget currently has a renderer. +// One will be created the first time a widget is shown but may be removed after it is hidden. +func IsRendered(wid fyne.Widget) bool { + _, found := renderers.Load(wid) + return found +} + +type rendererInfo struct { + expiringCache + renderer fyne.WidgetRenderer +} diff --git a/vendor/fyne.io/fyne/v2/internal/clip.go b/vendor/fyne.io/fyne/v2/internal/clip.go new file mode 100644 index 0000000..dc1ddbc --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/clip.go @@ -0,0 +1,89 @@ +package internal + +import "fyne.io/fyne/v2" + +// ClipStack keeps track of the areas that should be clipped when drawing a canvas. +// If no clips are present then adding one will be added as-is. +// Subsequent items pushed will be completely within the previous clip. +type ClipStack struct { + clips []*ClipItem +} + +// Pop removes the current top clip and returns it. +func (c *ClipStack) Pop() *ClipItem { + if len(c.clips) == 0 { + return nil + } + + top := len(c.clips) - 1 + ret := c.clips[top] + c.clips[top] = nil // release memory reference + c.clips = c.clips[:top] + return ret +} + +// Length returns the number of items in this clip stack. 0 means no clip. +func (c *ClipStack) Length() int { + return len(c.clips) +} + +// Push a new clip onto this stack at position and size specified. +// The returned clip item is the result of calculating the intersection of the requested clip and its parent. +func (c *ClipStack) Push(p fyne.Position, s fyne.Size) *ClipItem { + outer := c.Top() + inner := outer.Intersect(p, s) + + c.clips = append(c.clips, inner) + return inner +} + +// Top returns the current clip item - it will always be within the bounds of any parent clips. +func (c *ClipStack) Top() *ClipItem { + if len(c.clips) == 0 { + return nil + } + + return c.clips[len(c.clips)-1] +} + +// ClipItem represents a single clip in a clip stack, denoted by a size and position. +type ClipItem struct { + pos fyne.Position + size fyne.Size +} + +// Rect returns the position and size parameters of the clip. +func (i *ClipItem) Rect() (fyne.Position, fyne.Size) { + return i.pos, i.size +} + +// Intersect returns a new clip item that is the intersection of the requested parameters and this clip. +func (i *ClipItem) Intersect(p fyne.Position, s fyne.Size) *ClipItem { + ret := &ClipItem{p, s} + if i == nil { + return ret + } + + if ret.pos.X < i.pos.X { + ret.pos.X = i.pos.X + ret.size.Width -= i.pos.X - p.X + } + if ret.pos.Y < i.pos.Y { + ret.pos.Y = i.pos.Y + ret.size.Height -= i.pos.Y - p.Y + } + + if p.X+s.Width > i.pos.X+i.size.Width { + ret.size.Width = (i.pos.X + i.size.Width) - ret.pos.X + } + if p.Y+s.Height > i.pos.Y+i.size.Height { + ret.size.Height = (i.pos.Y + i.size.Height) - ret.pos.Y + } + + if ret.size.Width < 0 || ret.size.Height < 0 { + ret.size = fyne.NewSize(0, 0) + return ret + } + + return ret +} diff --git a/vendor/fyne.io/fyne/v2/internal/color/color.go b/vendor/fyne.io/fyne/v2/internal/color/color.go new file mode 100644 index 0000000..b0671c7 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/color/color.go @@ -0,0 +1,97 @@ +package color + +import ( + "image/color" +) + +// ToNRGBA converts a color to RGBA values which are not premultiplied, unlike color.RGBA(). +func ToNRGBA(c color.Color) (r, g, b, a int) { + // We use UnmultiplyAlpha with RGBA, RGBA64, and unrecognized implementations of Color. + // It works for all Colors whose RGBA() method is implemented according to spec, but is only necessary for those. + // Only RGBA and RGBA64 have components which are already premultiplied. + switch col := c.(type) { + // NRGBA and NRGBA64 are not premultiplied + case color.NRGBA: + r = int(col.R) + g = int(col.G) + b = int(col.B) + a = int(col.A) + case *color.NRGBA: + r = int(col.R) + g = int(col.G) + b = int(col.B) + a = int(col.A) + case color.NRGBA64: + r = int(col.R) >> 8 + g = int(col.G) >> 8 + b = int(col.B) >> 8 + a = int(col.A) >> 8 + case *color.NRGBA64: + r = int(col.R) >> 8 + g = int(col.G) >> 8 + b = int(col.B) >> 8 + a = int(col.A) >> 8 + // Gray and Gray16 have no alpha component + case *color.Gray: + r = int(col.Y) + g = int(col.Y) + b = int(col.Y) + a = 0xff + case color.Gray: + r = int(col.Y) + g = int(col.Y) + b = int(col.Y) + a = 0xff + case *color.Gray16: + r = int(col.Y) >> 8 + g = int(col.Y) >> 8 + b = int(col.Y) >> 8 + a = 0xff + case color.Gray16: + r = int(col.Y) >> 8 + g = int(col.Y) >> 8 + b = int(col.Y) >> 8 + a = 0xff + // Alpha and Alpha16 contain only an alpha component. + case color.Alpha: + r = 0xff + g = 0xff + b = 0xff + a = int(col.A) + case *color.Alpha: + r = 0xff + g = 0xff + b = 0xff + a = int(col.A) + case color.Alpha16: + r = 0xff + g = 0xff + b = 0xff + a = int(col.A) >> 8 + case *color.Alpha16: + r = 0xff + g = 0xff + b = 0xff + a = int(col.A) >> 8 + default: // RGBA, RGBA64, and unknown implementations of Color + r, g, b, a = unmultiplyAlpha(c) + } + return r, g, b, a +} + +// unmultiplyAlpha returns a color's RGBA components as 8-bit integers by calling c.RGBA() and then removing the alpha premultiplication. +// It is only used by ToRGBA. +func unmultiplyAlpha(c color.Color) (r, g, b, a int) { + red, green, blue, alpha := c.RGBA() + if alpha != 0 && alpha != 0xffff { + red = (red * 0xffff) / alpha + green = (green * 0xffff) / alpha + blue = (blue * 0xffff) / alpha + } + // Convert from range 0-65535 to range 0-255 + r = int(red >> 8) + g = int(green >> 8) + b = int(blue >> 8) + a = int(alpha >> 8) + return r, g, b, a +} diff --git a/vendor/fyne.io/fyne/v2/internal/docs.go b/vendor/fyne.io/fyne/v2/internal/docs.go new file mode 100644 index 0000000..a0ef109 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/docs.go @@ -0,0 +1,141 @@ +package internal + +import ( + "errors" + "net/url" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/storage" +) + +var errNoAppID = errors.New("storage API requires a unique ID, use app.NewWithID()") + +// Docs is an internal implementation of the document features of the Storage interface. +// It is based on top of the current `file` repository and is rooted at RootDocURI. +type Docs struct { + RootDocURI fyne.URI +} + +// Create will create a new document ready for writing, you must write something and close the returned writer +// for the create process to complete. +// If the document for this app with that name already exists a storage.ErrAlreadyExists error will be returned. +func (d *Docs) Create(name string) (fyne.URIWriteCloser, error) { + if d.RootDocURI == nil { + return nil, errNoAppID + } + + err := d.ensureRootExists() + if err != nil { + return nil, err + } + + u, err := d.childURI(name) + if err != nil { + return nil, err + } + + exists, err := storage.Exists(u) + if err != nil { + return nil, err + } + if exists { + return nil, storage.ErrAlreadyExists + } + + return storage.Writer(u) +} + +// List returns all documents that have been saved by the current application. +// Remember to use `app.NewWithID` so that your storage is unique. +func (d *Docs) List() []string { + if d.RootDocURI == nil { + return nil + } + + uris, err := storage.List(d.RootDocURI) + if err != nil { + return nil + } + + ret := make([]string, len(uris)) + for i, u := range uris { + ret[i] = u.Name() + if d.RootDocURI.Scheme() != "file" { + ret[i], _ = url.PathUnescape(u.Name()) + } + } + + return ret +} + +// Open will grant access to the contents of the named file. If an error occurs it is returned instead. +func (d *Docs) Open(name string) (fyne.URIReadCloser, error) { + if d.RootDocURI == nil { + return nil, errNoAppID + } + + u, err := d.childURI(name) + if err != nil { + return nil, err + } + + return storage.Reader(u) +} + +// Remove will delete the document with the specified name, if it exists +func (d *Docs) Remove(name string) error { + if d.RootDocURI == nil { + return errNoAppID + } + + u, err := d.childURI(name) + if err != nil { + return err + } + + return storage.Delete(u) +} + +// Save will open a document ready for writing, you close the returned writer for the save to complete. +// If the document for this app with that name does not exist a storage.ErrNotExists error will be returned. +func (d *Docs) Save(name string) (fyne.URIWriteCloser, error) { + if d.RootDocURI == nil { + return nil, errNoAppID + } + + u, err := d.childURI(name) + if err != nil { + return nil, err + } + + exists, err := storage.Exists(u) + if err != nil { + return nil, err + } + if !exists { + return nil, storage.ErrNotExists + } + + return storage.Writer(u) +} + +func (d *Docs) ensureRootExists() error { + exists, err := storage.Exists(d.RootDocURI) + if err != nil { + return err + } + if exists { + return nil + } + + return storage.CreateListable(d.RootDocURI) +} + +func (d *Docs) childURI(name string) (fyne.URI, error) { + encoded := name + if d.RootDocURI.Scheme() != "file" { + encoded = url.PathEscape(name) + } + + return storage.Child(d.RootDocURI, encoded) +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/common/canvas.go b/vendor/fyne.io/fyne/v2/internal/driver/common/canvas.go new file mode 100644 index 0000000..faf6827 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/common/canvas.go @@ -0,0 +1,483 @@ +package common + +import ( + "image/color" + "reflect" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/internal" + "fyne.io/fyne/v2/internal/app" + "fyne.io/fyne/v2/internal/async" + "fyne.io/fyne/v2/internal/cache" + "fyne.io/fyne/v2/internal/driver" + "fyne.io/fyne/v2/internal/painter/gl" + "fyne.io/fyne/v2/internal/theme" +) + +// SizeableCanvas defines a canvas with size related functions. +type SizeableCanvas interface { + fyne.Canvas + Resize(fyne.Size) + MinSize() fyne.Size +} + +// Canvas defines common canvas implementation. +type Canvas struct { + OnFocus func(obj fyne.Focusable) + OnUnfocus func() + + impl SizeableCanvas + + contentFocusMgr *app.FocusManager + menuFocusMgr *app.FocusManager + overlays overlayStack + + shortcut fyne.ShortcutHandler + + painter gl.Painter + + // Any object that requests to enter to the refresh queue should + // not be omitted as it is always a rendering task's decision + // for skipping frames or drawing calls. + // + // If an object failed to ender the refresh queue, the object may + // disappear or blink from the view at any frames. As of this reason, + // the refreshQueue is an unbounded queue which is able to cache + // arbitrary number of fyne.CanvasObject for the rendering. + refreshQueue deduplicatedObjectQueue + dirty bool + + mWindowHeadTree, contentTree, menuTree *renderCacheTree +} + +// AddShortcut adds a shortcut to the canvas. +func (c *Canvas) AddShortcut(shortcut fyne.Shortcut, handler func(shortcut fyne.Shortcut)) { + c.shortcut.AddShortcut(shortcut, handler) +} + +func (c *Canvas) DrawDebugOverlay(obj fyne.CanvasObject, pos fyne.Position, size fyne.Size) { + switch obj.(type) { + case fyne.Widget: + r := canvas.NewRectangle(color.Transparent) + r.StrokeColor = color.NRGBA{R: 0xcc, G: 0x33, B: 0x33, A: 0xff} + r.StrokeWidth = 1 + r.Resize(obj.Size()) + c.Painter().Paint(r, pos, size) + + t := canvas.NewText(reflect.ValueOf(obj).Elem().Type().Name(), r.StrokeColor) + t.TextSize = 10 + c.Painter().Paint(t, pos.AddXY(2, 2), size) + case *fyne.Container: + r := canvas.NewRectangle(color.Transparent) + r.StrokeColor = color.NRGBA{R: 0x33, G: 0x33, B: 0xcc, A: 0xff} + r.StrokeWidth = 1 + r.Resize(obj.Size()) + c.Painter().Paint(r, pos, size) + } +} + +// EnsureMinSize ensure canvas min size. +// +// This function uses lock. +func (c *Canvas) EnsureMinSize() bool { + if c.impl.Content() == nil { + return false + } + windowNeedsMinSizeUpdate := false + csize := c.impl.Size() + min := c.impl.MinSize() + + var parentNeedingUpdate *RenderCacheNode + + setup := func(node *RenderCacheNode, pos fyne.Position) { + if !node.obj.Visible() { + return + } + if th, ok := node.Obj().(*container.ThemeOverride); ok { + theme.PushRenderingTheme(th.Theme) + } + } + ensureMinSize := func(node *RenderCacheNode, pos fyne.Position) { + obj := node.obj + cache.SetCanvasForObject(obj, c.impl, func() { + if img, ok := obj.(*canvas.Image); ok { + img.Refresh() // this may now have a different texScale + } + }) + + if parentNeedingUpdate == node { + c.updateLayout(obj) + parentNeedingUpdate = nil + } + + if !obj.Visible() { + return + } + minSize := obj.MinSize() + + minSizeChanged := node.minSize != minSize + if minSizeChanged { + node.minSize = minSize + if node.parent != nil { + parentNeedingUpdate = node.parent + } else { + windowNeedsMinSizeUpdate = true + size := obj.Size() + expectedSize := minSize.Max(size) + if expectedSize != size && size != csize { + obj.Resize(expectedSize) + } else { + c.updateLayout(obj) + } + } + } + + if _, ok := node.Obj().(*container.ThemeOverride); ok { + theme.PopRenderingTheme() + } + } + c.WalkTrees(setup, ensureMinSize) + + shouldResize := windowNeedsMinSizeUpdate && (csize.Width < min.Width || csize.Height < min.Height) + if shouldResize { + c.impl.Resize(csize.Max(min)) + } + return windowNeedsMinSizeUpdate +} + +// Focus makes the provided item focused. +func (c *Canvas) Focus(obj fyne.Focusable) { + focusMgr := c.focusManager() + if focusMgr != nil && focusMgr.Focus(obj) { // fast path – probably >99.9% of all cases + if c.OnFocus != nil { + c.OnFocus(obj) + } + return + } + + focusMgrs := append([]*app.FocusManager{c.contentFocusMgr, c.menuFocusMgr}, c.overlays.ListFocusManagers()...) + + for _, mgr := range focusMgrs { + if mgr == nil { + continue + } + if focusMgr != mgr && mgr.Focus(obj) { + if c.OnFocus != nil { + c.OnFocus(obj) + } + return + } + } + + fyne.LogError("Failed to focus object which is not part of the canvas’ content, menu or overlays.", nil) +} + +// Focused returns the current focused object. +func (c *Canvas) Focused() fyne.Focusable { + mgr := c.focusManager() + if mgr == nil { + return nil + } + return mgr.Focused() +} + +// FocusGained signals to the manager that its content got focus. +// Valid only on Desktop. +func (c *Canvas) FocusGained() { + mgr := c.focusManager() + if mgr == nil { + return + } + mgr.FocusGained() +} + +// FocusLost signals to the manager that its content lost focus. +// Valid only on Desktop. +func (c *Canvas) FocusLost() { + mgr := c.focusManager() + if mgr == nil { + return + } + mgr.FocusLost() +} + +// FocusNext focuses the next focusable item. +func (c *Canvas) FocusNext() { + mgr := c.focusManager() + if mgr == nil { + return + } + mgr.FocusNext() +} + +// FocusPrevious focuses the previous focusable item. +func (c *Canvas) FocusPrevious() { + mgr := c.focusManager() + if mgr == nil { + return + } + mgr.FocusPrevious() +} + +// FreeDirtyTextures frees dirty textures and returns the number of freed textures. +func (c *Canvas) FreeDirtyTextures() uint64 { + if c.painter == nil { + return 0 + } + + objectsToFree := c.refreshQueue.Len() + for object := c.refreshQueue.Out(); object != nil; object = c.refreshQueue.Out() { + c.freeObject(object) + } + + cache.RangeExpiredTexturesFor(c.impl, c.painter.Free) + return objectsToFree +} + +// Initialize initializes the canvas. +func (c *Canvas) Initialize(impl SizeableCanvas, onOverlayChanged func()) { + c.impl = impl + c.refreshQueue.queue = async.NewCanvasObjectQueue() + c.overlays.OverlayStack = internal.OverlayStack{ + OnChange: onOverlayChanged, + Canvas: impl, + } +} + +// ObjectTrees return canvas object trees. +// +// This function uses lock. +func (c *Canvas) ObjectTrees() []fyne.CanvasObject { + var content, menu fyne.CanvasObject + if c.contentTree != nil && c.contentTree.root != nil { + content = c.contentTree.root.obj + } + if c.menuTree != nil && c.menuTree.root != nil { + menu = c.menuTree.root.obj + } + trees := make([]fyne.CanvasObject, 0, len(c.Overlays().List())+2) + trees = append(trees, content) + if menu != nil { + trees = append(trees, menu) + } + trees = append(trees, c.Overlays().List()...) + return trees +} + +// Overlays returns the overlay stack. +func (c *Canvas) Overlays() fyne.OverlayStack { + return &c.overlays +} + +// Painter returns the canvas painter. +func (c *Canvas) Painter() gl.Painter { + return c.painter +} + +// Refresh refreshes a canvas object. +func (c *Canvas) Refresh(obj fyne.CanvasObject) { + c.refreshQueue.In(obj) + async.EnsureMain(c.SetDirty) +} + +// RemoveShortcut removes a shortcut from the canvas. +func (c *Canvas) RemoveShortcut(shortcut fyne.Shortcut) { + c.shortcut.RemoveShortcut(shortcut) +} + +// SetContentTreeAndFocusMgr sets content tree and focus manager. +// +// This function does not use the canvas lock. +func (c *Canvas) SetContentTreeAndFocusMgr(content fyne.CanvasObject) { + c.contentTree = &renderCacheTree{root: &RenderCacheNode{obj: content}} + + newFocusMgr := app.NewFocusManager(content) + if c.contentFocusMgr != nil { + focused := c.contentFocusMgr.Focused() + if focused != nil { + newFocusMgr.Focus(focused) // Focus old object if possible. + } + } + c.contentFocusMgr = newFocusMgr +} + +// CheckDirtyAndClear returns true if the canvas is dirty and +// clears the dirty state atomically. +func (c *Canvas) CheckDirtyAndClear() bool { + wasDirty := c.dirty + c.dirty = false + return wasDirty +} + +// SetDirty sets canvas dirty flag atomically. +func (c *Canvas) SetDirty() { + c.dirty = true +} + +// SetMenuTreeAndFocusMgr sets menu tree and focus manager. +// +// This function does not use the canvas lock. +func (c *Canvas) SetMenuTreeAndFocusMgr(menu fyne.CanvasObject) { + c.menuTree = &renderCacheTree{root: &RenderCacheNode{obj: menu}} + if menu != nil { + c.menuFocusMgr = app.NewFocusManager(menu) + } else { + c.menuFocusMgr = nil + } +} + +// SetMobileWindowHeadTree sets window head tree. +// +// This function does not use the canvas lock. +func (c *Canvas) SetMobileWindowHeadTree(head fyne.CanvasObject) { + c.mWindowHeadTree = &renderCacheTree{root: &RenderCacheNode{obj: head}} +} + +// SetPainter sets the canvas painter. +func (c *Canvas) SetPainter(p gl.Painter) { + c.painter = p +} + +// TypedShortcut handle the registered shortcut. +func (c *Canvas) TypedShortcut(shortcut fyne.Shortcut) { + c.shortcut.TypedShortcut(shortcut) +} + +// Unfocus unfocuses all the objects in the canvas. +func (c *Canvas) Unfocus() { + mgr := c.focusManager() + if mgr == nil { + return + } + if mgr.Focus(nil) && c.OnUnfocus != nil { + c.OnUnfocus() + } +} + +// WalkTrees walks over the trees. +func (c *Canvas) WalkTrees( + beforeChildren func(*RenderCacheNode, fyne.Position), + afterChildren func(*RenderCacheNode, fyne.Position), +) { + c.walkTree(c.contentTree, beforeChildren, afterChildren) + if c.mWindowHeadTree != nil && c.mWindowHeadTree.root.obj != nil { + c.walkTree(c.mWindowHeadTree, beforeChildren, afterChildren) + } + if c.menuTree != nil && c.menuTree.root.obj != nil { + c.walkTree(c.menuTree, beforeChildren, afterChildren) + } + for _, tree := range c.overlays.renderCaches { + if tree != nil { + c.walkTree(tree, beforeChildren, afterChildren) + } + } +} + +func (c *Canvas) focusManager() *app.FocusManager { + if focusMgr := c.overlays.TopFocusManager(); focusMgr != nil { + return focusMgr + } + if c.isMenuActive() { + return c.menuFocusMgr + } + return c.contentFocusMgr +} + +func (c *Canvas) freeObject(object fyne.CanvasObject) { + // Image.Refresh will trigger a refresh specific to the object, + // while recursing on parent widget would just lead to a double texture upload. + if img, ok := object.(*canvas.Image); ok { + c.painter.Free(img) + return + } + + driver.WalkCompleteObjectTree(object, func(obj fyne.CanvasObject, _ fyne.Position, _ fyne.Position, _ fyne.Size) bool { + if _, ok := obj.(*canvas.Image); !ok { // No image refresh while recursing to avoid double texture upload. + c.painter.Free(obj) + } + return false + }, nil) +} + +func (c *Canvas) isMenuActive() bool { + if c.menuTree == nil || c.menuTree.root == nil || c.menuTree.root.obj == nil { + return false + } + menu := c.menuTree.root.obj + if am, ok := menu.(activatableMenu); ok { + return am.IsActive() + } + return true +} + +func (c *Canvas) walkTree( + tree *renderCacheTree, + beforeChildren func(*RenderCacheNode, fyne.Position), + afterChildren func(*RenderCacheNode, fyne.Position), +) { + var node, parent, prev *RenderCacheNode + node = tree.root + + bc := func(obj fyne.CanvasObject, pos fyne.Position, _ fyne.Position, _ fyne.Size) bool { + if node != nil && node.obj != obj { + if parent.firstChild == node { + parent.firstChild = nil + } + node = nil + } + if node == nil { + node = &RenderCacheNode{parent: parent, obj: obj} + if parent.firstChild == nil { + parent.firstChild = node + } else { + prev.nextSibling = node + } + } + if prev != nil && prev.parent != parent { + prev = nil + } + + if beforeChildren != nil { + beforeChildren(node, pos) + } + + parent = node + node = parent.firstChild + return false + } + ac := func(obj fyne.CanvasObject, pos fyne.Position, _ fyne.CanvasObject) { + node = parent + parent = node.parent + if prev != nil && prev.parent != parent { + prev.nextSibling = nil + } + + if afterChildren != nil { + afterChildren(node, pos) + } + + prev = node + node = node.nextSibling + } + driver.WalkVisibleObjectTree(tree.root.obj, bc, ac) +} + +type activatableMenu interface { + IsActive() bool +} + +func (c *Canvas) updateLayout(objToLayout fyne.CanvasObject) { + switch cont := objToLayout.(type) { + case *fyne.Container: + if cont.Layout != nil { + layout := cont.Layout + objects := cont.Objects + layout.Layout(objects, cont.Size()) + } + case fyne.Widget: + renderer := cache.Renderer(cont) + renderer.Layout(cont.Size()) + } +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/common/driver.go b/vendor/fyne.io/fyne/v2/internal/driver/common/driver.go new file mode 100644 index 0000000..bcca9b8 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/common/driver.go @@ -0,0 +1,11 @@ +package common + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/cache" +) + +// CanvasForObject returns the canvas for the specified object. +func CanvasForObject(obj fyne.CanvasObject) fyne.Canvas { + return cache.GetCanvasForObject(obj) +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/common/structures.go b/vendor/fyne.io/fyne/v2/internal/driver/common/structures.go new file mode 100644 index 0000000..1073498 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/common/structures.go @@ -0,0 +1,95 @@ +package common + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal" + "fyne.io/fyne/v2/internal/async" + "fyne.io/fyne/v2/internal/build" +) + +type deduplicatedObjectQueue struct { + queue *async.CanvasObjectQueue + dedup async.Map[fyne.CanvasObject, struct{}] +} + +// In adds an object to the queue if it is not already present. +func (q *deduplicatedObjectQueue) In(obj fyne.CanvasObject) { + _, exists := q.dedup.Load(obj) + if exists { + return + } + + q.queue.In(obj) + q.dedup.Store(obj, struct{}{}) +} + +// Out removes and returns the next object from the queue. +// It assumes that the whole queue is drained and defers clearing +// the deduplication map until it is empty. +func (q *deduplicatedObjectQueue) Out() fyne.CanvasObject { + if q.queue.Len() == 0 { + q.dedup.Clear() + return nil + } + + out := q.queue.Out() + if !build.MigratedToFyneDo() { + q.dedup.Delete(out) + } + return out +} + +// Len returns the number of elements in the queue. +func (q *deduplicatedObjectQueue) Len() uint64 { + return q.queue.Len() +} + +type renderCacheTree struct { + root *RenderCacheNode +} + +// RenderCacheNode represents a node in a render cache tree. +type RenderCacheNode struct { + // structural data + firstChild *RenderCacheNode + nextSibling *RenderCacheNode + obj fyne.CanvasObject + parent *RenderCacheNode + // cache data + minSize fyne.Size +} + +// Obj returns the node object. +func (r *RenderCacheNode) Obj() fyne.CanvasObject { + return r.obj +} + +type overlayStack struct { + internal.OverlayStack + + renderCaches []*renderCacheTree +} + +func (o *overlayStack) Add(overlay fyne.CanvasObject) { + if overlay == nil { + return + } + + o.renderCaches = append(o.renderCaches, &renderCacheTree{root: &RenderCacheNode{obj: overlay}}) + o.OverlayStack.Add(overlay) +} + +func (o *overlayStack) Remove(overlay fyne.CanvasObject) { + if overlay == nil || len(o.List()) == 0 { + return + } + + o.OverlayStack.Remove(overlay) + overlayCount := len(o.List()) + + for i := overlayCount; i < len(o.renderCaches); i++ { + o.renderCaches[i] = nil // release memory reference to removed element + } + + o.renderCaches = o.renderCaches[:overlayCount] +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/common/window.go b/vendor/fyne.io/fyne/v2/internal/driver/common/window.go new file mode 100644 index 0000000..afa8946 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/common/window.go @@ -0,0 +1,11 @@ +package common + +import ( + "fyne.io/fyne/v2/internal/async" +) + +var DonePool = async.Pool[chan struct{}]{ + New: func() chan struct{} { + return make(chan struct{}) + }, +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/context.go b/vendor/fyne.io/fyne/v2/internal/driver/context.go new file mode 100644 index 0000000..925eabb --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/context.go @@ -0,0 +1,9 @@ +package driver + +// WithContext allows drivers to execute within another context. +// Mostly this helps GLFW code execute within the painter's GL context. +type WithContext interface { + RunWithContext(f func()) + RescaleContext() + Context() any +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/embedded/device.go b/vendor/fyne.io/fyne/v2/internal/driver/embedded/device.go new file mode 100644 index 0000000..5d6d91a --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/embedded/device.go @@ -0,0 +1,31 @@ +package embedded + +import ( + "fyne.io/fyne/v2" +) + +type noosDevice struct{} + +func (n *noosDevice) Orientation() fyne.DeviceOrientation { + return fyne.OrientationVertical +} + +func (n *noosDevice) IsMobile() bool { + return false +} + +func (n *noosDevice) IsBrowser() bool { + return false +} + +func (n *noosDevice) HasKeyboard() bool { + return true +} + +func (n *noosDevice) SystemScaleForWindow(fyne.Window) float32 { + return 1.0 +} + +func (n *noosDevice) Locale() fyne.Locale { + return "en" +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/embedded/driver.go b/vendor/fyne.io/fyne/v2/internal/driver/embedded/driver.go new file mode 100644 index 0000000..739ba9f --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/embedded/driver.go @@ -0,0 +1,241 @@ +package embedded + +import ( + "image" + "math" + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/driver/embedded" + "fyne.io/fyne/v2/internal/async" + "fyne.io/fyne/v2/internal/cache" + intdriver "fyne.io/fyne/v2/internal/driver" + "fyne.io/fyne/v2/internal/painter" +) + +type noosDriver struct { + events chan embedded.Event + queue chan funcData + render func(image.Image) + run func(func()) + size func() fyne.Size + done bool + + wins []fyne.Window + current int + device noosDevice +} + +func (n *noosDriver) CreateWindow(_ string) fyne.Window { + w := newWindow(n) + n.wins = append(n.wins, w) + n.current = len(n.wins) - 1 + + if f := n.size; f != nil { + w.Resize(f()) + } + return w +} + +func (n *noosDriver) AllWindows() []fyne.Window { + return n.wins +} + +func (n *noosDriver) RenderedTextSize(text string, fontSize float32, style fyne.TextStyle, source fyne.Resource) (size fyne.Size, baseline float32) { + return painter.RenderedTextSize(text, fontSize, style, source) +} + +func (n *noosDriver) CanvasForObject(obj fyne.CanvasObject) fyne.Canvas { + return cache.GetCanvasForObject(obj) +} + +func (n *noosDriver) AbsolutePositionForObject(o fyne.CanvasObject) fyne.Position { + c := n.CanvasForObject(o) + if c == nil { + return fyne.NewPos(0, 0) + } + + pos := intdriver.AbsolutePositionForObject(o, []fyne.CanvasObject{c.Content()}) + inset, _ := c.InteractiveArea() + return pos.Subtract(inset) +} + +func (n *noosDriver) Device() fyne.Device { + return &n.device +} + +func (n *noosDriver) Run() { + n.run(n.doRun) +} + +func (n *noosDriver) doRun() { + for _, w := range n.wins { + n.renderWindow(w) + } + + for !n.done { + select { + case fn := <-n.queue: + fn.f() + if fn.done != nil { + fn.done <- struct{}{} + } + case e := <-n.events: + if e == nil { + // closing + n.Quit() + continue + } + + w := n.wins[n.current].(*noosWindow) + + switch t := e.(type) { + case *embedded.CharacterEvent: + if focused := w.c.Focused(); focused != nil { + focused.TypedRune(t.Rune) + } else if tr := w.c.OnTypedRune(); tr != nil { + tr(t.Rune) + } + + n.renderWindow(n.wins[n.current]) + case *embedded.KeyEvent: + keyEvent := &fyne.KeyEvent{Name: t.Name} + + if t.Direction == embedded.KeyReleased { + // No desktop events so key/up down not reported + continue // ignore key up in other core events + } + + if t.Name == fyne.KeyTab { + captures := false + + if ent, ok := w.Canvas().Focused().(fyne.Tabbable); ok { + captures = ent.AcceptsTab() + } + if !captures { + // TODO handle shift + w.Canvas().FocusNext() + n.renderWindow(n.wins[n.current]) + continue + } + } + + // No shortcut detected, pass down to TypedKey + focused := w.c.Focused() + if focused != nil { + focused.TypedKey(keyEvent) + } else if tk := w.c.OnTypedKey(); tk != nil { + tk(keyEvent) + } + + n.renderWindow(n.wins[n.current]) + case *embedded.TouchDownEvent: + n.handleTouchDown(t, n.wins[n.current].(*noosWindow)) + case *embedded.TouchMoveEvent: + n.handleTouchMove(t, n.wins[n.current].(*noosWindow)) + case *embedded.TouchUpEvent: + n.handleTouchUp(t, n.wins[n.current].(*noosWindow)) + } + } + } +} + +func (n *noosDriver) handleTouchDown(ev *embedded.TouchDownEvent, w *noosWindow) { + w.c.tapDown(ev.Position, ev.ID) + n.renderWindow(w) +} + +func (n *noosDriver) handleTouchMove(ev *embedded.TouchMoveEvent, w *noosWindow) { + w.c.tapMove(ev.Position, ev.ID, func(wid fyne.Draggable, ev *fyne.DragEvent) { + wid.Dragged(ev) + }) + n.renderWindow(w) +} + +func (n *noosDriver) handleTouchUp(ev *embedded.TouchUpEvent, w *noosWindow) { + w.c.tapUp(ev.Position, ev.ID, func(wid fyne.Tappable, ev *fyne.PointEvent) { + wid.Tapped(ev) + }, func(wid fyne.SecondaryTappable, ev *fyne.PointEvent) { + wid.TappedSecondary(ev) + }, func(wid fyne.DoubleTappable, ev *fyne.PointEvent) { + wid.DoubleTapped(ev) + }, func(wid fyne.Draggable, ev *fyne.DragEvent) { + if math.Abs(float64(ev.Dragged.DX)) <= tapMoveEndThreshold && math.Abs(float64(ev.Dragged.DY)) <= tapMoveEndThreshold { + wid.DragEnd() + return + } + + go func() { + for math.Abs(float64(ev.Dragged.DX)) > tapMoveEndThreshold || math.Abs(float64(ev.Dragged.DY)) > tapMoveEndThreshold { + if math.Abs(float64(ev.Dragged.DX)) > 0 { + ev.Dragged.DX *= tapMoveDecay + } + if math.Abs(float64(ev.Dragged.DY)) > 0 { + ev.Dragged.DY *= tapMoveDecay + } + + n.DoFromGoroutine(func() { + wid.Dragged(ev) + }, false) + time.Sleep(time.Millisecond * 16) + } + + n.DoFromGoroutine(wid.DragEnd, false) + }() + }) + n.renderWindow(w) +} + +func (n *noosDriver) Quit() { + n.done = true + + go func() { + n.queue <- funcData{f: func() {}} + }() +} + +func (n *noosDriver) StartAnimation(*fyne.Animation) { + // no animations on embedded +} + +func (n *noosDriver) StopAnimation(*fyne.Animation) { + // no animations on embedded +} + +func (n *noosDriver) DoubleTapDelay() time.Duration { + return tapDoubleDelay +} + +func (n *noosDriver) SetDisableScreenBlanking(bool) {} + +func (n *noosDriver) DoFromGoroutine(fn func(), wait bool) { + if wait { + async.EnsureNotMain(func() { + done := make(chan struct{}) + + n.queue <- funcData{f: fn, done: done} + <-done + }) + } else { + n.queue <- funcData{f: fn} + } +} + +// TODO add some caching to stop allocating images... +func (n *noosDriver) renderWindow(w fyne.Window) { + img := w.Canvas().Capture() + + n.render(img) +} + +func NewNoOSDriver(render func(img image.Image), run func(func()), events chan embedded.Event, size func() fyne.Size) fyne.Driver { + return &noosDriver{ + events: events, queue: make(chan funcData), size: size, + render: render, run: run, wins: make([]fyne.Window, 0), + } +} + +type funcData struct { + f func() + done chan struct{} // Zero allocation signalling channel +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/embedded/touchscreen.go b/vendor/fyne.io/fyne/v2/internal/driver/embedded/touchscreen.go new file mode 100644 index 0000000..08ff3fd --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/embedded/touchscreen.go @@ -0,0 +1,236 @@ +package embedded + +import ( + "context" + "math" + "sync" + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/driver/mobile" + "fyne.io/fyne/v2/driver/software" + "fyne.io/fyne/v2/internal/driver" + "fyne.io/fyne/v2/test" +) + +const ( + tapMoveDecay = 0.92 // how much should the scroll continue decay on each frame? + tapMoveEndThreshold = 2.0 // at what offset will we stop decaying? + tapMoveThreshold = 4.0 // how far can we move before it is a drag + tapSecondaryDelay = 300 * time.Millisecond // how long before secondary tap + tapDoubleDelay = 500 * time.Millisecond // max duration between taps for a DoubleTap event +) + +type touchCanvas struct { + test.WindowlessCanvas + + lastTapDown map[int]time.Time + lastTapDownPos map[int]fyne.Position + lastTapDelta map[int]fyne.Delta + + dragOffset fyne.Position + dragStart fyne.Position + dragging fyne.Draggable + + touched map[int]mobile.Touchable + touchCancelFunc context.CancelFunc + touchCancelLock sync.Mutex + touchLastTapped fyne.CanvasObject + touchTapCount int +} + +func newTouchCanvas() *touchCanvas { + ret := &touchCanvas{ + WindowlessCanvas: software.NewCanvas(), + lastTapDown: make(map[int]time.Time), + lastTapDownPos: make(map[int]fyne.Position), + lastTapDelta: make(map[int]fyne.Delta), + touched: make(map[int]mobile.Touchable), + } + return ret +} + +func (c *touchCanvas) tapDown(pos fyne.Position, tapID int) { + c.lastTapDown[tapID] = time.Now() + c.lastTapDownPos[tapID] = pos + c.dragging = nil + + co, objPos, layer := driver.FindObjectAtPositionMatching(pos, func(object fyne.CanvasObject) bool { + switch object.(type) { + case mobile.Touchable, fyne.Focusable: + return true + } + + return false + }, nil, nil, c.Content()) + + if wid, ok := co.(mobile.Touchable); ok { + touchEv := &mobile.TouchEvent{} + touchEv.Position = objPos + touchEv.AbsolutePosition = pos + wid.TouchDown(touchEv) + c.touched[tapID] = wid + } + + if layer != 1 { // 0 - overlay, 1 - window head / menu, 2 - content + if wid, ok := co.(fyne.Focusable); !ok || wid != c.Focused() { + c.Unfocus() + } + } +} + +func (c *touchCanvas) tapMove(pos fyne.Position, tapID int, + dragCallback func(fyne.Draggable, *fyne.DragEvent), +) { + previousPos := c.lastTapDownPos[tapID] + deltaX := pos.X - previousPos.X + deltaY := pos.Y - previousPos.Y + + if c.dragging == nil && (math.Abs(float64(deltaX)) < tapMoveThreshold && math.Abs(float64(deltaY)) < tapMoveThreshold) { + return + } + c.lastTapDownPos[tapID] = pos + offset := fyne.Delta{DX: deltaX, DY: deltaY} + c.lastTapDelta[tapID] = offset + + co, objPos, _ := driver.FindObjectAtPositionMatching(pos, func(object fyne.CanvasObject) bool { + if _, ok := object.(fyne.Draggable); ok { + return true + } else if _, ok := object.(mobile.Touchable); ok { + return true + } + + return false + }, nil, nil, c.Content()) + + if c.touched[tapID] != nil { + if touch, ok := co.(mobile.Touchable); !ok || c.touched[tapID] != touch { + touchEv := &mobile.TouchEvent{} + touchEv.Position = objPos + touchEv.AbsolutePosition = pos + c.touched[tapID].TouchCancel(touchEv) + c.touched[tapID] = nil + } + } + + if c.dragging == nil { + if drag, ok := co.(fyne.Draggable); ok { + c.dragging = drag + c.dragOffset = previousPos.Subtract(objPos) + c.dragStart = co.Position() + } else { + return + } + } + + ev := &fyne.DragEvent{} + draggedObjDelta := c.dragStart.Subtract(c.dragging.(fyne.CanvasObject).Position()) + ev.Position = pos.Subtract(c.dragOffset).Add(draggedObjDelta) + ev.Dragged = offset + + dragCallback(c.dragging, ev) +} + +func (c *touchCanvas) tapUp(pos fyne.Position, tapID int, + tapCallback func(fyne.Tappable, *fyne.PointEvent), + tapAltCallback func(fyne.SecondaryTappable, *fyne.PointEvent), + doubleTapCallback func(fyne.DoubleTappable, *fyne.PointEvent), + dragCallback func(fyne.Draggable, *fyne.DragEvent), +) { + if c.dragging != nil { + previousDelta := c.lastTapDelta[tapID] + ev := &fyne.DragEvent{Dragged: previousDelta} + draggedObjDelta := c.dragStart.Subtract(c.dragging.(fyne.CanvasObject).Position()) + ev.Position = pos.Subtract(c.dragOffset).Add(draggedObjDelta) + ev.AbsolutePosition = pos + dragCallback(c.dragging, ev) + + c.dragging = nil + return + } + + duration := time.Since(c.lastTapDown[tapID]) + + co, objPos, _ := driver.FindObjectAtPositionMatching(pos, func(object fyne.CanvasObject) bool { + if _, ok := object.(fyne.Tappable); ok { + return true + } else if _, ok := object.(fyne.SecondaryTappable); ok { + return true + } else if _, ok := object.(mobile.Touchable); ok { + return true + } else if _, ok := object.(fyne.DoubleTappable); ok { + return true + } + + return false + }, nil, nil, c.Content()) + + if wid, ok := co.(mobile.Touchable); ok { + touchEv := &mobile.TouchEvent{} + touchEv.Position = objPos + touchEv.AbsolutePosition = pos + wid.TouchUp(touchEv) + c.touched[tapID] = nil + } + + ev := &fyne.PointEvent{ + Position: objPos, + AbsolutePosition: pos, + } + + if duration < tapSecondaryDelay { + _, doubleTap := co.(fyne.DoubleTappable) + if doubleTap { + c.touchCancelLock.Lock() + c.touchTapCount++ + c.touchLastTapped = co + cancel := c.touchCancelFunc + c.touchCancelLock.Unlock() + if cancel != nil { + cancel() + return + } + go c.waitForDoubleTap(co, ev, tapCallback, doubleTapCallback) + } else { + if wid, ok := co.(fyne.Tappable); ok { + tapCallback(wid, ev) + } + } + } else { + if wid, ok := co.(fyne.SecondaryTappable); ok { + tapAltCallback(wid, ev) + } + } +} + +func (c *touchCanvas) waitForDoubleTap(co fyne.CanvasObject, ev *fyne.PointEvent, tapCallback func(fyne.Tappable, *fyne.PointEvent), doubleTapCallback func(fyne.DoubleTappable, *fyne.PointEvent)) { + ctx, cancel := context.WithDeadline(context.TODO(), time.Now().Add(tapDoubleDelay)) + c.touchCancelLock.Lock() + c.touchCancelFunc = cancel + c.touchCancelLock.Unlock() + defer cancel() + + <-ctx.Done() + fyne.CurrentApp().Driver().DoFromGoroutine(func() { + c.touchCancelLock.Lock() + touchCount := c.touchTapCount + touchLast := c.touchLastTapped + c.touchCancelLock.Unlock() + + if touchCount == 2 && touchLast == co { + if wid, ok := co.(fyne.DoubleTappable); ok { + doubleTapCallback(wid, ev) + } + } else { + if wid, ok := co.(fyne.Tappable); ok { + tapCallback(wid, ev) + } + } + + c.touchCancelLock.Lock() + c.touchTapCount = 0 + c.touchCancelFunc = nil + c.touchLastTapped = nil + c.touchCancelLock.Unlock() + }, true) +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/embedded/window.go b/vendor/fyne.io/fyne/v2/internal/driver/embedded/window.go new file mode 100644 index 0000000..768a2f4 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/embedded/window.go @@ -0,0 +1,137 @@ +package embedded + +import ( + "fyne.io/fyne/v2" +) + +type noosWindow struct { + c *touchCanvas + d *noosDriver + + title string +} + +func (w *noosWindow) Title() string { + return w.title +} + +func (w *noosWindow) SetTitle(s string) { + w.title = s +} + +func (w *noosWindow) FullScreen() bool { + return true +} + +func (w *noosWindow) SetFullScreen(_ bool) { +} + +func (w *noosWindow) Resize(s fyne.Size) { + w.c.Resize(s) +} + +func (w *noosWindow) RequestFocus() { + // TODO implement me + panic("implement me") +} + +func (w *noosWindow) FixedSize() bool { + return true +} + +func (w *noosWindow) SetFixedSize(bool) {} + +func (w *noosWindow) CenterOnScreen() {} + +func (w *noosWindow) Padded() bool { + return w.c.Padded() +} + +func (w *noosWindow) SetPadded(pad bool) { + w.c.SetPadded(pad) +} + +func (w *noosWindow) Icon() fyne.Resource { + // TODO implement me + return nil +} + +func (w *noosWindow) SetIcon(fyne.Resource) { + // TODO implement me +} + +func (w *noosWindow) SetMaster() { + // TODO implement me +} + +func (w *noosWindow) MainMenu() *fyne.MainMenu { + // TODO implement me + return nil +} + +func (w *noosWindow) SetMainMenu(menu *fyne.MainMenu) { + // TODO implement me +} + +func (w *noosWindow) SetOnClosed(f func()) { + // TODO implement me +} + +func (w *noosWindow) SetCloseIntercept(f func()) { + // TODO implement me +} + +func (w *noosWindow) SetOnDropped(func(fyne.Position, []fyne.URI)) {} + +func (w *noosWindow) Show() { + w.d.renderWindow(w) +} + +func (w *noosWindow) Hide() {} + +func (w *noosWindow) Close() { + i := -1 + for _, win := range w.d.wins { + if win == w { + break + } + i++ + } + if i == -1 { + return + } + + copy(w.d.wins[i:], w.d.wins[i+1:]) + w.d.wins[len(w.d.wins)-1] = nil // Allow the garbage collector to reclaim the memory. + w.d.wins = w.d.wins[:len(w.d.wins)-1] + + if w.d.current > 0 { + w.d.current-- + } +} + +func (w *noosWindow) ShowAndRun() { + w.Show() + w.d.Run() +} + +func (w *noosWindow) Content() fyne.CanvasObject { + return w.c.Content() +} + +func (w *noosWindow) SetContent(object fyne.CanvasObject) { + w.c.SetContent(object) +} + +func (w *noosWindow) Canvas() fyne.Canvas { + return w.c +} + +func (w *noosWindow) Clipboard() fyne.Clipboard { + // TODO implement me + return nil +} + +func newWindow(d *noosDriver) fyne.Window { + return &noosWindow{c: newTouchCanvas(), d: d} +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/animation.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/animation.go new file mode 100644 index 0000000..ec7d72a --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/animation.go @@ -0,0 +1,11 @@ +package glfw + +import "fyne.io/fyne/v2" + +func (d *gLDriver) StartAnimation(a *fyne.Animation) { + d.animation.Start(a) +} + +func (d *gLDriver) StopAnimation(a *fyne.Animation) { + d.animation.Stop(a) +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/canvas.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/canvas.go new file mode 100644 index 0000000..69af54a --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/canvas.go @@ -0,0 +1,312 @@ +package glfw + +import ( + "image" + "math" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/internal" + "fyne.io/fyne/v2/internal/app" + "fyne.io/fyne/v2/internal/build" + "fyne.io/fyne/v2/internal/driver" + "fyne.io/fyne/v2/internal/driver/common" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" +) + +// Declare conformity with Canvas interface +var _ fyne.Canvas = (*glCanvas)(nil) + +type glCanvas struct { + common.Canvas + + content fyne.CanvasObject + menu fyne.CanvasObject + padded bool + size fyne.Size + + onTypedRune func(rune) + onTypedKey func(*fyne.KeyEvent) + onKeyDown func(*fyne.KeyEvent) + onKeyUp func(*fyne.KeyEvent) + // shortcut fyne.ShortcutHandler + + scale, detectedScale, texScale float32 + + context driver.WithContext + webExtraWindows *container.MultipleWindows +} + +func (c *glCanvas) Capture() image.Image { + var img image.Image + c.context.(*window).RunWithContext(func() { + img = c.Painter().Capture(c) + }) + return img +} + +func (c *glCanvas) Content() fyne.CanvasObject { + return c.content +} + +func (c *glCanvas) DismissMenu() bool { + if c.menu != nil && c.menu.(*MenuBar).IsActive() { + c.menu.(*MenuBar).Toggle() + return true + } + return false +} + +func (c *glCanvas) InteractiveArea() (fyne.Position, fyne.Size) { + return fyne.Position{}, c.Size() +} + +func (c *glCanvas) MinSize() fyne.Size { + return c.canvasSize(c.content.MinSize()) +} + +func (c *glCanvas) OnKeyDown() func(*fyne.KeyEvent) { + return c.onKeyDown +} + +func (c *glCanvas) OnKeyUp() func(*fyne.KeyEvent) { + return c.onKeyUp +} + +func (c *glCanvas) OnTypedKey() func(*fyne.KeyEvent) { + return c.onTypedKey +} + +func (c *glCanvas) OnTypedRune() func(rune) { + return c.onTypedRune +} + +func (c *glCanvas) Padded() bool { + return c.padded +} + +func (c *glCanvas) PixelCoordinateForPosition(pos fyne.Position) (int, int) { + multiple := c.scale * c.texScale + scaleInt := func(x float32) int { + return int(math.Round(float64(x * multiple))) + } + + return scaleInt(pos.X), scaleInt(pos.Y) +} + +func (c *glCanvas) Resize(size fyne.Size) { + // This might not be the ideal solution, but it effectively avoid the first frame to be blurry due to the + // rounding of the size to the loower integer when scale == 1. It does not affect the other cases as far as we tested. + // This can easily be seen with fyne/cmd/hello and a scale == 1 as the text will happear blurry without the following line. + nearestSize := fyne.NewSize(float32(math.Ceil(float64(size.Width))), float32(math.Ceil(float64(size.Height)))) + + c.size = nearestSize + + if c.webExtraWindows != nil { + c.webExtraWindows.Resize(size) + } + for _, overlay := range c.Overlays().List() { + if p, ok := overlay.(*widget.PopUp); ok { + // TODO: remove this when #707 is being addressed. + // “Notifies” the PopUp of the canvas size change. + p.Refresh() + } else { + overlay.Resize(nearestSize) + } + } + + content := c.content + contentSize := c.contentSize(nearestSize) + contentPos := c.contentPos() + menu := c.menu + menuHeight := c.menuHeight() + + content.Resize(contentSize) + content.Move(contentPos) + + if menu != nil { + menu.Refresh() + menu.Resize(fyne.NewSize(nearestSize.Width, menuHeight)) + } +} + +func (c *glCanvas) Scale() float32 { + return c.scale +} + +func (c *glCanvas) SetContent(content fyne.CanvasObject) { + content.Resize(content.MinSize()) // give it the space it wants then calculate the real min + + // the pass above makes some layouts wide enough to wrap, so we ask again what the true min is. + newSize := c.size.Max(c.canvasSize(content.MinSize())) + + c.setContent(content) + + c.Resize(newSize) + c.SetDirty() +} + +func (c *glCanvas) SetOnKeyDown(typed func(*fyne.KeyEvent)) { + c.onKeyDown = typed +} + +func (c *glCanvas) SetOnKeyUp(typed func(*fyne.KeyEvent)) { + c.onKeyUp = typed +} + +func (c *glCanvas) SetOnTypedKey(typed func(*fyne.KeyEvent)) { + c.onTypedKey = typed +} + +func (c *glCanvas) SetOnTypedRune(typed func(rune)) { + c.onTypedRune = typed +} + +func (c *glCanvas) SetPadded(padded bool) { + c.padded = padded + + c.content.Move(c.contentPos()) +} + +func (c *glCanvas) reloadScale() { + w := c.context.(*window) + windowVisible := w.visible + if !windowVisible { + return + } + + c.scale = w.calculatedScale() + c.SetDirty() + + c.context.RescaleContext() +} + +func (c *glCanvas) Size() fyne.Size { + return c.size +} + +func (c *glCanvas) ToggleMenu() { + if c.menu != nil { + c.menu.(*MenuBar).Toggle() + } +} + +func (c *glCanvas) buildMenu(w *window, m *fyne.MainMenu) { + c.setMenuOverlay(nil) + if m == nil { + return + } + if build.HasNativeMenu { + setupNativeMenu(w, m) + } else { + c.setMenuOverlay(buildMenuOverlay(m, w)) + } +} + +// canvasSize computes the needed canvas size for the given content size +func (c *glCanvas) canvasSize(contentSize fyne.Size) fyne.Size { + canvasSize := contentSize.Add(fyne.NewSize(0, c.menuHeight())) + if c.Padded() { + return canvasSize.Add(fyne.NewSquareSize(theme.Padding() * 2)) + } + return canvasSize +} + +func (c *glCanvas) contentPos() fyne.Position { + contentPos := fyne.NewPos(0, c.menuHeight()) + if c.Padded() { + return contentPos.Add(fyne.NewSquareOffsetPos(theme.Padding())) + } + return contentPos +} + +func (c *glCanvas) contentSize(canvasSize fyne.Size) fyne.Size { + contentSize := fyne.NewSize(canvasSize.Width, canvasSize.Height-c.menuHeight()) + if c.Padded() { + return contentSize.Subtract(fyne.NewSquareSize(theme.Padding() * 2)) + } + return contentSize +} + +func (c *glCanvas) menuHeight() float32 { + if c.menu == nil { + return 0 // no menu or native menu -> does not consume space on the canvas + } + + return c.menu.MinSize().Height +} + +func (c *glCanvas) overlayChanged() { + c.SetDirty() +} + +func (c *glCanvas) paint(size fyne.Size) { + clips := &internal.ClipStack{} + if c.Content() == nil { + return + } + c.Painter().Clear() + + paint := func(node *common.RenderCacheNode, pos fyne.Position) { + obj := node.Obj() + if driver.IsClip(obj) { + inner := clips.Push(pos, obj.Size()) + c.Painter().StartClipping(inner.Rect()) + } + if size.Width <= 0 || size.Height <= 0 { // iconifying on Windows can do bad things + return + } + c.Painter().Paint(obj, pos, size) + } + afterPaint := func(node *common.RenderCacheNode, pos fyne.Position) { + if driver.IsClip(node.Obj()) { + clips.Pop() + if top := clips.Top(); top != nil { + c.Painter().StartClipping(top.Rect()) + } else { + c.Painter().StopClipping() + } + } + + if build.Mode == fyne.BuildDebug { + c.DrawDebugOverlay(node.Obj(), pos, size) + } + } + c.WalkTrees(paint, afterPaint) +} + +func (c *glCanvas) setContent(content fyne.CanvasObject) { + c.content = content + c.SetContentTreeAndFocusMgr(content) +} + +func (c *glCanvas) setMenuOverlay(b fyne.CanvasObject) { + c.menu = b + c.SetMenuTreeAndFocusMgr(b) + + if c.menu != nil && !c.size.IsZero() { + c.content.Resize(c.contentSize(c.size)) + c.content.Move(c.contentPos()) + + c.menu.Refresh() + c.menu.Resize(fyne.NewSize(c.size.Width, c.menu.MinSize().Height)) + } +} + +func (c *glCanvas) applyThemeOutOfTreeObjects() { + if c.menu != nil { + app.ApplyThemeTo(c.menu, c) // Ensure our menu gets the theme change message as it's out-of-tree + } + + c.SetPadded(c.padded) // refresh the padding for potential theme differences +} + +func newCanvas() *glCanvas { + c := &glCanvas{scale: 1.0, texScale: 1.0, padded: true} + connectKeyboard(c) + c.Initialize(c, c.overlayChanged) + c.setContent(&canvas.Rectangle{FillColor: theme.Color(theme.ColorNameBackground)}) + return c +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/clipboard.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/clipboard.go new file mode 100644 index 0000000..9171a5b --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/clipboard.go @@ -0,0 +1,64 @@ +//go:build !wasm && !test_web_driver + +package glfw + +import ( + "runtime" + "time" + + "fyne.io/fyne/v2" + + "github.com/go-gl/glfw/v3.3/glfw" +) + +// Declare conformity with Clipboard interface +var _ fyne.Clipboard = clipboard{} + +func NewClipboard() fyne.Clipboard { + return clipboard{} +} + +// clipboard represents the system clipboard +type clipboard struct{} + +// Content returns the clipboard content +func (c clipboard) Content() string { + // This retry logic is to work around the "Access Denied" error often thrown in windows PR#1679 + if runtime.GOOS != "windows" { + return c.content() + } + for i := 3; i > 0; i-- { + cb := c.content() + if cb != "" { + return cb + } + time.Sleep(50 * time.Millisecond) + } + // can't log retry as it would also log errors for an empty clipboard + return "" +} + +func (c clipboard) content() string { + return glfw.GetClipboardString() +} + +// SetContent sets the clipboard content +func (c clipboard) SetContent(content string) { + // This retry logic is to work around the "Access Denied" error often thrown in windows PR#1679 + if runtime.GOOS != "windows" { + c.setContent(content) + return + } + for i := 3; i > 0; i-- { + c.setContent(content) + if c.content() == content { + return + } + time.Sleep(50 * time.Millisecond) + } + fyne.LogError("GLFW clipboard set failed", nil) +} + +func (c clipboard) setContent(content string) { + glfw.SetClipboardString(content) +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/clipboard_wasm.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/clipboard_wasm.go new file mode 100644 index 0000000..84594af --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/clipboard_wasm.go @@ -0,0 +1,28 @@ +//go:build wasm || test_web_driver + +package glfw + +import ( + "fyne.io/fyne/v2" + "github.com/fyne-io/glfw-js" +) + +// Declare conformity with Clipboard interface +var _ fyne.Clipboard = clipboard{} + +func NewClipboard() fyne.Clipboard { + return clipboard{} +} + +// clipboard represents the system clipboard +type clipboard struct{} + +// Content returns the clipboard content +func (c clipboard) Content() string { + return glfw.GetClipboardString() +} + +// SetContent sets the clipboard content +func (c clipboard) SetContent(content string) { + glfw.SetClipboardString(content) +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/device.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/device.go new file mode 100644 index 0000000..e732b1b --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/device.go @@ -0,0 +1,29 @@ +package glfw + +import ( + "runtime" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/lang" +) + +type glDevice struct{} + +// Declare conformity with Device +var _ fyne.Device = (*glDevice)(nil) + +func (*glDevice) Locale() fyne.Locale { + return lang.SystemLocale() +} + +func (*glDevice) Orientation() fyne.DeviceOrientation { + return fyne.OrientationHorizontalLeft // TODO should we consider the monitor orientation or topmost window? +} + +func (*glDevice) HasKeyboard() bool { + return true // TODO actually check - we could be in tablet mode +} + +func (*glDevice) IsBrowser() bool { + return runtime.GOARCH == "js" || runtime.GOOS == "js" +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/device_desktop.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/device_desktop.go new file mode 100644 index 0000000..cff8ba9 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/device_desktop.go @@ -0,0 +1,33 @@ +//go:build !wasm + +package glfw + +import ( + "runtime" + + "fyne.io/fyne/v2" +) + +func (*glDevice) IsMobile() bool { + return false +} + +func (*glDevice) SystemScaleForWindow(w fyne.Window) float32 { + if runtime.GOOS == "darwin" { + return 1.0 // macOS scaling is done at the texture level + } + if runtime.GOOS == "windows" { + xScale, _ := w.(*window).viewport.GetContentScale() + return xScale + } + + return scaleAuto +} + +func connectKeyboard(*glCanvas) { + // no-op, mobile web compatibility +} + +func isMacOSRuntime() bool { + return runtime.GOOS == "darwin" +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/device_wasm.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/device_wasm.go new file mode 100644 index 0000000..cd2cf13 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/device_wasm.go @@ -0,0 +1,75 @@ +//go:build wasm + +package glfw + +import ( + "regexp" + "strings" + "syscall/js" + + "fyne.io/fyne/v2" +) + +var ( + isMobile bool + isMacOS bool + + setCursor func(name string) + + blurDummyEntry func(...any) js.Value + focusDummyEntry func(...any) js.Value +) + +func init() { + navigator := js.Global().Get("navigator") + isMobile = regexp.MustCompile("Android|iPhone|iPad|iPod"). + MatchString(navigator.Get("userAgent").String()) + isMacOS = strings.Contains(navigator.Get("platform").String(), "Mac") + + document := js.Global().Get("document") + style := document.Get("body").Get("style") + setStyleProperty := style.Get("setProperty").Call("bind", style) + setCursor = func(name string) { + setStyleProperty.Invoke("cursor", name) + } + + dummyEntry := document.Call("getElementById", "dummyEntry") + if dummyEntry.IsNull() { + return + } + + blurDummyEntry = dummyEntry.Get("blur").Call("bind", dummyEntry).Invoke + focusDummyEntry = dummyEntry.Get("focus").Call("bind", dummyEntry).Invoke +} + +func (*glDevice) IsMobile() bool { + return isMobile +} + +func (*glDevice) SystemScaleForWindow(w fyne.Window) float32 { + // Get the scale information from the web browser directly + return float32(js.Global().Get("devicePixelRatio").Float()) +} + +func (*glDevice) hideVirtualKeyboard() { + if blurDummyEntry == nil { + return + } + blurDummyEntry() +} + +func (*glDevice) showVirtualKeyboard() { + if focusDummyEntry == nil { + return + } + focusDummyEntry() +} + +func connectKeyboard(c *glCanvas) { + c.OnFocus = handleKeyboard + c.OnUnfocus = hideVirtualKeyboard +} + +func isMacOSRuntime() bool { + return isMacOS // Value depends on which OS the browser is running on. +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/driver.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/driver.go new file mode 100644 index 0000000..624e346 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/driver.go @@ -0,0 +1,181 @@ +// Package glfw provides a full Fyne desktop driver that uses the system OpenGL libraries. +// This supports Windows, Mac OS X and Linux using the gl and glfw packages from go-gl. +package glfw + +import ( + "bytes" + "image" + "os" + "runtime" + + "fyne.io/fyne/v2/internal/async" + "github.com/fyne-io/image/ico" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/animation" + intapp "fyne.io/fyne/v2/internal/app" + "fyne.io/fyne/v2/internal/driver" + "fyne.io/fyne/v2/internal/driver/common" + "fyne.io/fyne/v2/internal/painter" + intRepo "fyne.io/fyne/v2/internal/repository" + "fyne.io/fyne/v2/storage/repository" +) + +var curWindow *window + +// Declare conformity with Driver +var _ fyne.Driver = (*gLDriver)(nil) + +type gLDriver struct { + windows []fyne.Window + initialized bool + done chan struct{} + + animation animation.Runner + + currentKeyModifiers fyne.KeyModifier // desktop driver only + + trayStart, trayStop func() // shut down the system tray, if used + systrayMenu *fyne.Menu // cache the menu set so we know when to refresh +} + +func (d *gLDriver) init() { + if !d.initialized { + d.initialized = true + d.initGLFW() + } +} + +func toOSIcon(icon []byte) ([]byte, error) { + if runtime.GOOS != "windows" { + return icon, nil + } + + img, _, err := image.Decode(bytes.NewReader(icon)) + if err != nil { + return nil, err + } + + buf := &bytes.Buffer{} + err = ico.Encode(buf, img) + if err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +func (d *gLDriver) DoFromGoroutine(f func(), wait bool) { + if wait { + async.EnsureNotMain(func() { + runOnMainWithWait(f, true) + }) + } else { + runOnMainWithWait(f, false) + } +} + +func (d *gLDriver) RenderedTextSize(text string, textSize float32, style fyne.TextStyle, source fyne.Resource) (size fyne.Size, baseline float32) { + return painter.RenderedTextSize(text, textSize, style, source) +} + +func (d *gLDriver) CanvasForObject(obj fyne.CanvasObject) fyne.Canvas { + return common.CanvasForObject(obj) +} + +func (d *gLDriver) AbsolutePositionForObject(co fyne.CanvasObject) fyne.Position { + c := d.CanvasForObject(co) + if c == nil { + return fyne.NewPos(0, 0) + } + + glc := c.(*glCanvas) + return driver.AbsolutePositionForObject(co, glc.ObjectTrees()) +} + +func (d *gLDriver) Device() fyne.Device { + return &glDevice{} +} + +func (d *gLDriver) Quit() { + if curWindow != nil { + if f := fyne.CurrentApp().Lifecycle().(*intapp.Lifecycle).OnExitedForeground(); f != nil { + f() + } + curWindow = nil + if d.trayStop != nil { + d.trayStop() + } + } + + // Only call close once to avoid panic. + if running.CompareAndSwap(true, false) { + close(d.done) + } +} + +func (d *gLDriver) addWindow(w *window) { + d.windows = append(d.windows, w) +} + +// a trivial implementation of "focus previous" - return to the most recently opened, or master if set. +// This may not do the right thing if your app has 3 or more windows open, but it was agreed this was not much +// of an issue, and the added complexity to track focus was not needed at this time. +func (d *gLDriver) focusPreviousWindow() { + var chosen *window + for _, w := range d.windows { + win := w.(*window) + if !win.visible { + continue + } + chosen = win + if win.master { + break + } + } + + if chosen == nil || chosen.view() == nil { + return + } + chosen.RequestFocus() +} + +func (d *gLDriver) windowList() []fyne.Window { + return d.windows +} + +func (d *gLDriver) initFailed(msg string, err error) { + fyne.LogError(msg, err) + + if !running.Load() { + d.Quit() + } else { + os.Exit(1) + } +} + +func (d *gLDriver) Run() { + if !async.IsMainGoroutine() { + panic("Run() or ShowAndRun() must be called from main goroutine") + } + + go d.catchTerm() + d.runGL() + + // Ensure lifecycle events run to completion before the app exits + l := fyne.CurrentApp().Lifecycle().(*intapp.Lifecycle) + l.WaitForEvents() + l.DestroyEventQueue() +} + +func (d *gLDriver) SetDisableScreenBlanking(disable bool) { + setDisableScreenBlank(disable) +} + +// NewGLDriver sets up a new Driver instance implemented using the GLFW Go library and OpenGL bindings. +func NewGLDriver() *gLDriver { + repository.Register("file", intRepo.NewFileRepository()) + + return &gLDriver{ + done: make(chan struct{}), + } +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/driver_darwin.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/driver_darwin.go new file mode 100644 index 0000000..58cbae2 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/driver_darwin.go @@ -0,0 +1,21 @@ +//go:build darwin + +package glfw + +/* +#import + +void setDisableDisplaySleep(bool); +double doubleClickInterval(); +*/ +import "C" +import "time" + +func setDisableScreenBlank(disable bool) { + C.setDisableDisplaySleep(C.bool(disable)) +} + +func (d *gLDriver) DoubleTapDelay() time.Duration { + millis := int64(float64(C.doubleClickInterval()) * 1000) + return time.Duration(millis) * time.Millisecond +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/driver_darwin.m b/vendor/fyne.io/fyne/v2/internal/driver/glfw/driver_darwin.m new file mode 100644 index 0000000..884aae3 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/driver_darwin.m @@ -0,0 +1,34 @@ +//go:build darwin + +#import +#import + +IOPMAssertionID currentDisableID; + +void setDisableDisplaySleep(BOOL disable) { + if (!disable) { + if (currentDisableID == 0) { + return; + } + + IOPMAssertionRelease(currentDisableID); + currentDisableID = 0; + return; + } + + if (currentDisableID != 0) { + return; + } + IOPMAssertionID assertionID; + IOReturn success = IOPMAssertionCreateWithName(kIOPMAssertionTypeNoDisplaySleep, + kIOPMAssertionLevelOn, (CFStringRef)@"App disabled screensaver", &assertionID); + + if (success == kIOReturnSuccess) { + currentDisableID = assertionID; + } +} + +// https://developer.apple.com/documentation/appkit/nsevent/1528384-doubleclickinterval?language=objc +double doubleClickInterval() { + return [NSEvent doubleClickInterval]; +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/driver_desktop.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/driver_desktop.go new file mode 100644 index 0000000..04adbd8 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/driver_desktop.go @@ -0,0 +1,233 @@ +//go:build !wasm && !test_web_driver + +package glfw + +import ( + "bytes" + "image/png" + "os" + "os/signal" + "runtime" + "syscall" + + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/internal/painter" + "fyne.io/fyne/v2/internal/svg" + "fyne.io/fyne/v2/lang" + "fyne.io/systray" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/theme" +) + +var ( + systrayIcon fyne.Resource + systrayRunning bool +) + +func (d *gLDriver) SetSystemTrayMenu(m *fyne.Menu) { + if !systrayRunning { + systrayRunning = true + d.runSystray(m) + } + + d.refreshSystray(m) +} + +func (d *gLDriver) runSystray(m *fyne.Menu) { + d.trayStart, d.trayStop = systray.RunWithExternalLoop(func() { + if systrayIcon != nil { + d.SetSystemTrayIcon(systrayIcon) + } else if fyne.CurrentApp().Icon() != nil { + d.SetSystemTrayIcon(fyne.CurrentApp().Icon()) + } else { + d.SetSystemTrayIcon(theme.BrokenImageIcon()) + } + + // Some XDG systray crash without a title (See #3678) + if runtime.GOOS == "linux" || runtime.GOOS == "openbsd" || runtime.GOOS == "freebsd" || runtime.GOOS == "netbsd" { + app := fyne.CurrentApp() + title := app.Metadata().Name + if title == "" { + title = app.UniqueID() + } + + systray.SetTitle(title) + } + + if m != nil { + // it must be refreshed after init, so an earlier call would have been ineffective + runOnMain(func() { + d.refreshSystray(m) + }) + } + }, func() { + // anything required for tear-down + }) + + // the only way we know the app was asked to quit is if this window is asked to close... + w := d.CreateWindow("SystrayMonitor") + w.(*window).create() + w.SetCloseIntercept(d.Quit) +} + +func itemForMenuItem(i *fyne.MenuItem, parent *systray.MenuItem) *systray.MenuItem { + if i.IsSeparator { + if parent != nil { + parent.AddSeparator() + } else { + systray.AddSeparator() + } + return nil + } + + var item *systray.MenuItem + if i.Checked { + if parent != nil { + item = parent.AddSubMenuItemCheckbox(i.Label, "", true) + } else { + item = systray.AddMenuItemCheckbox(i.Label, "", true) + } + } else { + if parent != nil { + item = parent.AddSubMenuItem(i.Label, "") + } else { + item = systray.AddMenuItem(i.Label, "") + } + } + if i.Disabled { + item.Disable() + } + if i.Icon != nil { + data := i.Icon.Content() + if svg.IsResourceSVG(i.Icon) { + b := &bytes.Buffer{} + res := i.Icon + if runtime.GOOS == "windows" && isDark() { // windows menus don't match dark mode so invert icons + res = theme.NewInvertedThemedResource(i.Icon) + } + img := painter.PaintImage(canvas.NewImageFromResource(res), nil, 64, 64) + err := png.Encode(b, img) + if err != nil { + fyne.LogError("Failed to encode SVG icon for menu", err) + } else { + data = b.Bytes() + } + } + + img, err := toOSIcon(data) + if err != nil { + fyne.LogError("Failed to convert systray icon", err) + } else { + if _, ok := i.Icon.(*theme.ThemedResource); ok { + item.SetTemplateIcon(img, img) + } else { + item.SetIcon(img) + } + } + } + return item +} + +func (d *gLDriver) refreshSystray(m *fyne.Menu) { + d.systrayMenu = m + + systray.ResetMenu() + d.refreshSystrayMenu(m, nil) + + addMissingQuitForMenu(m, d) +} + +func (d *gLDriver) refreshSystrayMenu(m *fyne.Menu, parent *systray.MenuItem) { + if m == nil { + return + } + + for _, i := range m.Items { + item := itemForMenuItem(i, parent) + if item == nil { + continue // separator + } + if i.ChildMenu != nil { + d.refreshSystrayMenu(i.ChildMenu, item) + } + + fn := i.Action + go func() { + for range item.ClickedCh { + if fn != nil { + runOnMain(fn) + } + } + }() + } +} + +func (d *gLDriver) SetSystemTrayIcon(resource fyne.Resource) { + systrayIcon = resource // in case we need it later + + img, err := toOSIcon(resource.Content()) + if err != nil { + fyne.LogError("Failed to convert systray icon", err) + return + } + + if _, ok := resource.(*theme.ThemedResource); ok { + systray.SetTemplateIcon(img, img) + } else { + systray.SetIcon(img) + } +} + +func (d *gLDriver) SetSystemTrayWindow(w fyne.Window) { + if !systrayRunning { + systrayRunning = true + d.runSystray(nil) + } + + w.SetCloseIntercept(w.Hide) + glw := w.(*window) + if glw.decorate { + systray.SetOnTapped(glw.Show) + } else { + systray.SetOnTapped(glw.toggleVisible) + } +} + +func (d *gLDriver) SystemTrayMenu() *fyne.Menu { + return d.systrayMenu +} + +func (d *gLDriver) CurrentKeyModifiers() fyne.KeyModifier { + return d.currentKeyModifiers +} + +// this function should be invoked from a goroutine +func (d *gLDriver) catchTerm() { + terminateSignal := make(chan os.Signal, 1) + signal.Notify(terminateSignal, syscall.SIGINT, syscall.SIGTERM) + + <-terminateSignal + fyne.Do(d.Quit) +} + +func addMissingQuitForMenu(menu *fyne.Menu, d *gLDriver) { + localQuit := lang.L("Quit") + var lastItem *fyne.MenuItem + if len(menu.Items) > 0 { + lastItem = menu.Items[len(menu.Items)-1] + if lastItem.Label == localQuit { + lastItem.IsQuit = true + } + } + if lastItem == nil || !lastItem.IsQuit { // make sure the menu always has a quit option + quitItem := fyne.NewMenuItem(localQuit, nil) + quitItem.IsQuit = true + menu.Items = append(menu.Items, fyne.NewMenuItemSeparator(), quitItem) + } + for _, item := range menu.Items { + if item.IsQuit && item.Action == nil { + item.Action = d.Quit + } + } +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/driver_notwindows.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/driver_notwindows.go new file mode 100644 index 0000000..436cc1f --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/driver_notwindows.go @@ -0,0 +1,7 @@ +//go:build !windows + +package glfw + +func isDark() bool { + return true // this is really a no-op placeholder for a windows menu workaround +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/driver_wasm.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/driver_wasm.go new file mode 100644 index 0000000..77357b6 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/driver_wasm.go @@ -0,0 +1,25 @@ +//go:build wasm || test_web_driver + +package glfw + +import ( + "time" + + "fyne.io/fyne/v2" +) + +const webDefaultDoubleTapDelay = 300 * time.Millisecond + +func (d *gLDriver) SetSystemTrayMenu(m *fyne.Menu) { + // no-op for wasm apps using this driver +} + +func (d *gLDriver) catchTerm() {} + +func setDisableScreenBlank(disable bool) { + // awaiting complete support for WakeLock +} + +func (d *gLDriver) DoubleTapDelay() time.Duration { + return webDefaultDoubleTapDelay +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/driver_windows.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/driver_windows.go new file mode 100644 index 0000000..609126b --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/driver_windows.go @@ -0,0 +1,80 @@ +package glfw + +import ( + "fmt" + "runtime" + "syscall" + "time" + "unsafe" +) + +type ( + MB uint32 + ES uint +) + +const ( + MB_OK MB = 0x0000_0000 + MB_ICONERROR MB = 0x0000_0010 + + ES_CONTINUOUS ES = 0x80000000 + ES_DISPLAY_REQUIRED ES = 0x00000002 +) + +var ( + kernel32 = syscall.NewLazyDLL("kernel32.dll") + user32 = syscall.NewLazyDLL("user32.dll") + + executionState = kernel32.NewProc("SetThreadExecutionState") + MessageBox = user32.NewProc("MessageBoxW") + getDoubleClickTime = user32.NewProc("GetDoubleClickTime") +) + +func toNativePtr(s string) *uint16 { + pstr, err := syscall.UTF16PtrFromString(s) + if err != nil { + panic(fmt.Sprintf("toNativePtr() failed \"%s\": %s", s, err)) + } + return pstr +} + +// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-messageboxw +func messageBoxError(text, caption string) { + uType := MB_OK | MB_ICONERROR + + syscall.SyscallN(MessageBox.Addr(), + uintptr(unsafe.Pointer(nil)), uintptr(unsafe.Pointer(toNativePtr(text))), + uintptr(unsafe.Pointer(toNativePtr(caption))), uintptr(uType)) +} + +func logError(msg string, err error) { + text := fmt.Sprintf("Fyne error: %v", msg) + if err != nil { + text = text + fmt.Sprintf("\n Cause:%v", err) + } + + _, file, line, ok := runtime.Caller(1) + if ok { + text = text + fmt.Sprintf("\n At: %s:%d", file, line) + } + + messageBoxError(text, "Fyne Error") +} + +func setDisableScreenBlank(disable bool) { + uType := ES_CONTINUOUS + if disable { + uType |= ES_DISPLAY_REQUIRED + } + + syscall.SyscallN(executionState.Addr(), uintptr(uType)) +} + +func (d *gLDriver) DoubleTapDelay() time.Duration { + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getdoubleclicktime + if getDoubleClickTime == nil { + return desktopDefaultDoubleTapDelay + } + r1, _, _ := syscall.SyscallN(getDoubleClickTime.Addr()) + return time.Duration(uint64(r1) * uint64(time.Millisecond)) +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/driver_xdg.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/driver_xdg.go new file mode 100644 index 0000000..502724c --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/driver_xdg.go @@ -0,0 +1,51 @@ +//go:build linux || freebsd || openbsd || netbsd + +package glfw + +import "C" + +import ( + "time" + + "github.com/godbus/dbus/v5" + + "fyne.io/fyne/v2" +) + +var inhibitCookie uint32 + +func setDisableScreenBlank(disable bool) { + conn, err := dbus.SessionBus() // shared connection, don't close + if err != nil { + fyne.LogError("Unable to connect to session D-Bus", err) + return + } + + if !disable { + if inhibitCookie != 0 { + obj := conn.Object("org.freedesktop.ScreenSaver", "/org/freedesktop/ScreenSaver") + call := obj.Call("org.freedesktop.ScreenSaver.UnInhibit", 0, inhibitCookie) + if call.Err != nil { + fyne.LogError("Failed to send message to bus", call.Err) + } + inhibitCookie = 0 + } + return + } + + if inhibitCookie != 0 { + return + } + obj := conn.Object("org.freedesktop.ScreenSaver", "/org/freedesktop/ScreenSaver") + call := obj.Call("org.freedesktop.ScreenSaver.Inhibit", 0, fyne.CurrentApp().Metadata().ID, + "App disabled screensaver") + if call.Err == nil { + inhibitCookie = call.Body[0].(uint32) + } else { + fyne.LogError("Failed to send message to bus", call.Err) + } +} + +func (d *gLDriver) DoubleTapDelay() time.Duration { + return desktopDefaultDoubleTapDelay +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/glfw_core.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/glfw_core.go new file mode 100644 index 0000000..4a0b1a1 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/glfw_core.go @@ -0,0 +1,12 @@ +//go:build ((!gles && !arm && !arm64) || darwin) && !wasm && !test_web_driver + +package glfw + +import "github.com/go-gl/glfw/v3.3/glfw" + +func initWindowHints() { + glfw.WindowHint(glfw.ContextVersionMajor, 2) + glfw.WindowHint(glfw.ContextVersionMinor, 1) + + glfw.WindowHint(glfw.CocoaGraphicsSwitching, glfw.True) +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/glfw_es.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/glfw_es.go new file mode 100644 index 0000000..d3a7b11 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/glfw_es.go @@ -0,0 +1,11 @@ +//go:build (gles || arm || arm64) && !darwin && !wasm && !test_web_driver + +package glfw + +import "github.com/go-gl/glfw/v3.3/glfw" + +func initWindowHints() { + glfw.WindowHint(glfw.ClientAPI, glfw.OpenGLESAPI) + glfw.WindowHint(glfw.ContextVersionMajor, 2) + glfw.WindowHint(glfw.ContextVersionMinor, 0) +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/glfw_wasm.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/glfw_wasm.go new file mode 100644 index 0000000..aec734c --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/glfw_wasm.go @@ -0,0 +1,5 @@ +//go:build wasm || test_web_driver + +package glfw + +func initWindowHints() {} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/key.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/key.go new file mode 100644 index 0000000..a83a110 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/key.go @@ -0,0 +1,13 @@ +package glfw + +// Action represents the change of state of a key or mouse button event +type action int + +const ( + // Release Keyboard button was released + release action = 0 + // Press Keyboard button was pressed + press action = 1 + // Repeat Keyboard button was hold pressed for long enough that it trigger a repeat + repeat action = 2 +) diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/keyboard.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/keyboard.go new file mode 100644 index 0000000..768a439 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/keyboard.go @@ -0,0 +1,31 @@ +//go:build wasm + +package glfw + +import ( + "fyne.io/fyne/v2" +) + +func hideVirtualKeyboard() { + if d, ok := fyne.CurrentDevice().(*glDevice); ok { + d.hideVirtualKeyboard() + } +} + +func handleKeyboard(obj fyne.Focusable) { + isDisabled := false + if disWid, ok := obj.(fyne.Disableable); ok { + isDisabled = disWid.Disabled() + } + if obj != nil && !isDisabled { + showVirtualKeyboard() + } else { + hideVirtualKeyboard() + } +} + +func showVirtualKeyboard() { + if d, ok := fyne.CurrentDevice().(*glDevice); ok { + d.showVirtualKeyboard() + } +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/loop.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/loop.go new file mode 100644 index 0000000..d66fb4f --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/loop.go @@ -0,0 +1,243 @@ +package glfw + +import ( + "runtime" + "sync/atomic" + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/app" + "fyne.io/fyne/v2/internal/async" + "fyne.io/fyne/v2/internal/cache" + "fyne.io/fyne/v2/internal/driver/common" + "fyne.io/fyne/v2/internal/painter" + "fyne.io/fyne/v2/internal/scale" +) + +type funcData struct { + f func() + done chan struct{} // Zero allocation signalling channel +} + +// channel for queuing functions on the main thread +var ( + funcQueue = async.NewUnboundedChan[funcData]() + running, drained atomic.Bool +) + +// Arrange that main.main runs on main thread. +func init() { + runtime.LockOSThread() + async.SetMainGoroutine() +} + +// force a function f to run on the main thread +func runOnMain(f func()) { + runOnMainWithWait(f, true) +} + +// force a function f to run on the main thread and specify if we should wait for it to return +func runOnMainWithWait(f func(), wait bool) { + // If we are on main before app run just execute - otherwise add it to the main queue and wait. + // We also need to run it as-is if the app is in the process of shutting down as the queue will be stopped. + if (!running.Load() && async.IsMainGoroutine()) || drained.Load() { + f() + return + } + + if wait { + done := common.DonePool.Get() + defer common.DonePool.Put(done) + + funcQueue.In() <- funcData{f: f, done: done} + <-done + } else { + funcQueue.In() <- funcData{f: f} + } +} + +func (d *gLDriver) drawSingleFrame() { + refreshed := false + for _, win := range d.windowList() { + w := win.(*window) + if w.closing { + continue + } + + // CheckDirtyAndClear must be checked after visibility, + // because when a window becomes visible, it could be + // showing old content without a dirty flag set to true. + // Do the clear if and only if the window is visible. + if !w.visible || !w.canvas.CheckDirtyAndClear() { + // Window hidden or not being redrawn, mark canvasForObject + // cache alive if it hasn't been done recently + // n.b. we need to make sure threshold is a bit *after* + // time.Now() - CacheDuration() + threshold := time.Now().Add(10*time.Second - cache.ValidDuration) + if w.lastWalkedTime.Before(threshold) { + w.canvas.WalkTrees(nil, func(node *common.RenderCacheNode, _ fyne.Position) { + // marks canvas for object cache entry alive + _ = cache.GetCanvasForObject(node.Obj()) + // marks renderer cache entry alive + if wid, ok := node.Obj().(fyne.Widget); ok { + _, _ = cache.CachedRenderer(wid) + } + }) + w.lastWalkedTime = time.Now() + } + continue + } + + w.RunWithContext(func() { + if w.driver.repaintWindow(w) { + refreshed = true + } + }) + } + cache.Clean(refreshed) +} + +func (d *gLDriver) runGL() { + if !running.CompareAndSwap(false, true) { + return // Run was called twice. + } + + d.init() + if d.trayStart != nil { + d.trayStart() + } + + fyne.CurrentApp().Settings().AddListener(func(set fyne.Settings) { + painter.ClearFontCache() + cache.ResetThemeCaches() + app.ApplySettingsWithCallback(set, fyne.CurrentApp(), func(w fyne.Window) { + c, ok := w.Canvas().(*glCanvas) + if !ok { + return + } + c.applyThemeOutOfTreeObjects() + c.reloadScale() + }) + }) + + if f := fyne.CurrentApp().Lifecycle().(*app.Lifecycle).OnStarted(); f != nil { + f() + } + + eventTick := time.NewTicker(time.Second / 60) + for { + select { + case <-d.done: + eventTick.Stop() + d.Terminate() + l := fyne.CurrentApp().Lifecycle().(*app.Lifecycle) + if f := l.OnStopped(); f != nil { + l.QueueEvent(f) + } + + // as we are shutting down make sure we drain the pending funcQueue and close it out. + for len(funcQueue.Out()) > 0 { + f := <-funcQueue.Out() + if f.done != nil { + f.done <- struct{}{} + } + } + drained.Store(true) + funcQueue.Close() + return + case f := <-funcQueue.Out(): + f.f() + if f.done != nil { + f.done <- struct{}{} + } + case <-eventTick.C: + d.pollEvents() + for i := 0; i < len(d.windows); i++ { + w := d.windows[i].(*window) + if w.viewport == nil { + continue + } + + if w.viewport.ShouldClose() { + d.destroyWindow(w, i) + i-- // Trailing windows are moved forward one step. + continue + } + + expand := w.shouldExpand + fullScreen := w.fullScreen + + if expand && !fullScreen { + w.fitContent() + shouldExpand := w.shouldExpand + w.shouldExpand = false + view := w.viewport + + if shouldExpand && runtime.GOOS != "js" { + view.SetSize(w.shouldWidth, w.shouldHeight) + } + } + } + + d.animation.TickAnimations() + d.drawSingleFrame() + } + } +} + +func (d *gLDriver) destroyWindow(w *window, index int) { + w.visible = false + w.viewport.Destroy() + w.destroy(d) + + if index < len(d.windows)-1 { + copy(d.windows[index:], d.windows[index+1:]) + } + d.windows[len(d.windows)-1] = nil + d.windows = d.windows[:len(d.windows)-1] + + if len(d.windows) == 0 { + d.Quit() + } +} + +func (d *gLDriver) repaintWindow(w *window) bool { + canvas := w.canvas + freed := false + if canvas.EnsureMinSize() { + w.shouldExpand = true + } + freed = canvas.FreeDirtyTextures() > 0 + + updateGLContext(w) + canvas.paint(canvas.Size()) + + view := w.viewport + visible := w.visible + + if view != nil && visible { + view.SwapBuffers() + } + + // mark that we have walked the window and don't + // need to walk it again to mark caches alive + w.lastWalkedTime = time.Now() + return freed +} + +// refreshWindow requests that the specified window be redrawn +func refreshWindow(w *window) { + w.canvas.SetDirty() +} + +func updateGLContext(w *window) { + canvas := w.canvas + size := canvas.Size() + + // w.width and w.height are not correct if we are maximised, so figure from canvas + winWidth := float32(scale.ToScreenCoordinate(canvas, size.Width)) * canvas.texScale + winHeight := float32(scale.ToScreenCoordinate(canvas, size.Height)) * canvas.texScale + + canvas.Painter().SetFrameBufferScale(canvas.texScale) + canvas.Painter().SetOutputSize(int(winWidth), int(winHeight)) +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/loop_desktop.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/loop_desktop.go new file mode 100644 index 0000000..5c22301 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/loop_desktop.go @@ -0,0 +1,27 @@ +//go:build !wasm && !test_web_driver + +package glfw + +import ( + "fyne.io/fyne/v2" + + "github.com/go-gl/glfw/v3.3/glfw" +) + +func (d *gLDriver) initGLFW() { + err := glfw.Init() + if err != nil { + fyne.LogError("failed to initialise GLFW", err) + return + } + + initCursors() +} + +func (d *gLDriver) pollEvents() { + glfw.PollEvents() // This call blocks while window is being resized, which prevents freeDirtyTextures from being called +} + +func (d *gLDriver) Terminate() { + glfw.Terminate() +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/loop_wasm.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/loop_wasm.go new file mode 100644 index 0000000..6536115 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/loop_wasm.go @@ -0,0 +1,26 @@ +//go:build wasm || test_web_driver + +package glfw + +import ( + "fyne.io/fyne/v2" + + "github.com/fyne-io/gl-js" + "github.com/fyne-io/glfw-js" +) + +func (d *gLDriver) initGLFW() { + err := glfw.Init(gl.ContextWatcher) + if err != nil { + fyne.LogError("failed to initialise GLFW", err) + return + } +} + +func (d *gLDriver) pollEvents() { + glfw.PollEvents() // This call blocks while window is being resized, which prevents freeDirtyTextures from being called +} + +func (d *gLDriver) Terminate() { + glfw.Terminate() +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/menu.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/menu.go new file mode 100644 index 0000000..9c1008c --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/menu.go @@ -0,0 +1,15 @@ +package glfw + +import ( + "fyne.io/fyne/v2" +) + +func buildMenuOverlay(menus *fyne.MainMenu, w *window) fyne.CanvasObject { + if len(menus.Items) == 0 { + fyne.LogError("Main menu must have at least one child menu", nil) + return nil + } + + menus = addMissingQuitForMainMenu(menus, w) + return NewMenuBar(menus, w.canvas) +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/menu_bar.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/menu_bar.go new file mode 100644 index 0000000..02caddf --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/menu_bar.go @@ -0,0 +1,209 @@ +package glfw + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/driver/desktop" + "fyne.io/fyne/v2/internal/widget" + "fyne.io/fyne/v2/theme" +) + +var _ fyne.Widget = (*MenuBar)(nil) + +// MenuBar is a widget for displaying a fyne.MainMenu in a bar. +type MenuBar struct { + widget.Base + Items []fyne.CanvasObject + + active bool + activeItem *menuBarItem + canvas fyne.Canvas +} + +// NewMenuBar creates a menu bar populated with items from the passed main menu structure. +func NewMenuBar(mainMenu *fyne.MainMenu, canvas fyne.Canvas) *MenuBar { + items := make([]fyne.CanvasObject, len(mainMenu.Items)) + b := &MenuBar{Items: items, canvas: canvas} + b.ExtendBaseWidget(b) + for i, menu := range mainMenu.Items { + barItem := &menuBarItem{Menu: menu, Parent: b} + barItem.ExtendBaseWidget(barItem) + items[i] = barItem + } + return b +} + +// CreateRenderer returns a new renderer for the menu bar. +func (b *MenuBar) CreateRenderer() fyne.WidgetRenderer { + cont := container.NewHBox(b.Items...) + background := canvas.NewRectangle(theme.Color(theme.ColorNameBackground)) + underlay := &menuBarUnderlay{action: b.deactivate} + underlay.ExtendBaseWidget(underlay) + objects := []fyne.CanvasObject{underlay, background, cont} + for _, item := range b.Items { + objects = append(objects, item.(*menuBarItem).Child()) + } + return &menuBarRenderer{ + widget.NewShadowingRenderer(objects, widget.MenuLevel), + b, + background, + underlay, + cont, + } +} + +// IsActive returns whether the menu bar is active or not. +// An active menu bar shows the current selected menu and should have the focus. +func (b *MenuBar) IsActive() bool { + return b.active +} + +// Toggle changes the activation state of the menu bar. +// On activation, the first item will become active. +func (b *MenuBar) Toggle() { + b.toggle(b.Items[0].(*menuBarItem)) +} + +func (b *MenuBar) activateChild(item *menuBarItem) { + b.active = true + if item.Child() != nil { + item.Child().DeactivateChild() + } + if b.activeItem == item { + return + } + + if b.activeItem != nil { + if c := b.activeItem.Child(); c != nil { + c.Hide() + } + b.activeItem.Refresh() + } + b.activeItem = item + if item == nil { + return + } + + item.Refresh() + item.Child().Show() + b.Refresh() +} + +func (b *MenuBar) deactivate() { + if !b.active { + return + } + + b.active = false + if b.activeItem != nil { + if c := b.activeItem.Child(); c != nil { + defer c.Dismiss() + c.Hide() + } + b.activeItem.Refresh() + b.activeItem = nil + } + b.Refresh() +} + +func (b *MenuBar) toggle(item *menuBarItem) { + if b.active { + b.canvas.Unfocus() + b.deactivate() + } else { + b.activateChild(item) + b.canvas.Focus(item) + } +} + +type menuBarRenderer struct { + *widget.ShadowingRenderer + b *MenuBar + background *canvas.Rectangle + underlay *menuBarUnderlay + cont *fyne.Container +} + +func (r *menuBarRenderer) Layout(size fyne.Size) { + r.LayoutShadow(size, fyne.NewPos(0, 0)) + minSize := r.MinSize() + if size.Height != minSize.Height || size.Width < minSize.Width { + r.b.Resize(fyne.NewSize(fyne.Max(size.Width, minSize.Width), minSize.Height)) + return + } + + if r.b.active { + r.underlay.Resize(r.b.canvas.Size()) + } else { + r.underlay.Resize(fyne.NewSize(0, 0)) + } + innerPadding := theme.InnerPadding() + r.cont.Resize(fyne.NewSize(size.Width-2*innerPadding, size.Height)) + r.cont.Move(fyne.NewPos(innerPadding, 0)) + if item := r.b.activeItem; item != nil { + if item.Child().Size().IsZero() { + item.Child().Resize(item.Child().MinSize()) + } + item.Child().Move(fyne.NewPos(item.Position().X+innerPadding, item.Size().Height)) + } + r.background.Resize(size) +} + +func (r *menuBarRenderer) MinSize() fyne.Size { + return r.cont.MinSize().Add(fyne.NewSize(theme.InnerPadding()*2, 0)) +} + +func (r *menuBarRenderer) Refresh() { + r.Layout(r.b.Size()) + r.background.FillColor = theme.Color(theme.ColorNameBackground) + r.background.Refresh() + r.ShadowingRenderer.RefreshShadow() + canvas.Refresh(r.b) +} + +// Transparent underlay shown as soon as menu is active. +// It catches mouse events outside the menu's objects. +type menuBarUnderlay struct { + widget.Base + action func() +} + +var ( + _ fyne.Widget = (*menuBarUnderlay)(nil) + _ fyne.Tappable = (*menuBarUnderlay)(nil) // deactivate menu on click outside + _ desktop.Hoverable = (*menuBarUnderlay)(nil) // block hover events on main content +) + +func (u *menuBarUnderlay) CreateRenderer() fyne.WidgetRenderer { + return &menuUnderlayRenderer{} +} + +func (u *menuBarUnderlay) MouseIn(*desktop.MouseEvent) { +} + +func (u *menuBarUnderlay) MouseOut() { +} + +func (u *menuBarUnderlay) MouseMoved(*desktop.MouseEvent) { +} + +func (u *menuBarUnderlay) Tapped(*fyne.PointEvent) { + u.action() +} + +type menuUnderlayRenderer struct { + widget.BaseRenderer +} + +var _ fyne.WidgetRenderer = (*menuUnderlayRenderer)(nil) + +func (r *menuUnderlayRenderer) Layout(fyne.Size) { +} + +func (r *menuUnderlayRenderer) MinSize() fyne.Size { + return fyne.NewSize(0, 0) +} + +func (r *menuUnderlayRenderer) Refresh() { +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/menu_bar_item.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/menu_bar_item.go new file mode 100644 index 0000000..89095d6 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/menu_bar_item.go @@ -0,0 +1,172 @@ +package glfw + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/driver/desktop" + "fyne.io/fyne/v2/internal/widget" + "fyne.io/fyne/v2/theme" + publicWidget "fyne.io/fyne/v2/widget" +) + +var ( + _ fyne.Widget = (*menuBarItem)(nil) + _ desktop.Hoverable = (*menuBarItem)(nil) + _ fyne.Focusable = (*menuBarItem)(nil) +) + +// menuBarItem is a widget for displaying an item for a fyne.Menu in a MenuBar. +type menuBarItem struct { + widget.Base + Menu *fyne.Menu + Parent *MenuBar + + active bool + child *publicWidget.Menu + hovered bool +} + +func (i *menuBarItem) Child() *publicWidget.Menu { + if i.child == nil { + child := publicWidget.NewMenu(i.Menu) + child.Hide() + child.OnDismiss = i.Parent.deactivate + i.child = child + } + return i.child +} + +// CreateRenderer returns a new renderer for the menu bar item. +func (i *menuBarItem) CreateRenderer() fyne.WidgetRenderer { + background := canvas.NewRectangle(theme.Color(theme.ColorNameHover)) + background.CornerRadius = theme.SelectionRadiusSize() + background.Hide() + text := canvas.NewText(i.Menu.Label, theme.Color(theme.ColorNameForeground)) + objects := []fyne.CanvasObject{background, text} + + return &menuBarItemRenderer{ + widget.NewBaseRenderer(objects), + i, + text, + background, + } +} + +func (i *menuBarItem) FocusGained() { + i.active = true + if i.Parent.active { + i.Parent.activateChild(i) + } + i.Refresh() +} + +func (i *menuBarItem) FocusLost() { + i.active = false + i.Refresh() +} + +func (i *menuBarItem) Focused() bool { + return i.active +} + +// MouseIn activates the item and shows the menu if the bar is active. +// The menu that was displayed before will be hidden. +// +// If the bar is not active, the item will be hovered. +func (i *menuBarItem) MouseIn(_ *desktop.MouseEvent) { + i.hovered = true + if i.Parent.active { + i.Parent.canvas.Focus(i) + } + i.Refresh() +} + +// MouseMoved activates the item and shows the menu if the bar is active. +// The menu that was displayed before will be hidden. +// This might have an effect when mouse and keyboard control are mixed. +// Changing the active menu with the keyboard will make the hovered menu bar item inactive. +// On the next mouse move the hovered item is activated again. +// +// If the bar is not active, this will do nothing. +func (i *menuBarItem) MouseMoved(_ *desktop.MouseEvent) { + if i.Parent.active { + i.Parent.canvas.Focus(i) + } +} + +// MouseOut does nothing if the bar is active. +// +// IF the bar is not active, it changes the item to not be hovered. +func (i *menuBarItem) MouseOut() { + i.hovered = false + i.Refresh() +} + +// Tapped toggles the activation state of the menu bar. +// It shows the item’s menu if the bar is activated and hides it if the bar is deactivated. +func (i *menuBarItem) Tapped(*fyne.PointEvent) { + i.Parent.toggle(i) +} + +func (i *menuBarItem) TypedKey(event *fyne.KeyEvent) { + switch event.Name { + case fyne.KeyLeft: + if !i.Child().DeactivateLastSubmenu() { + i.Parent.canvas.FocusPrevious() + } + case fyne.KeyRight: + if !i.Child().ActivateLastSubmenu() { + i.Parent.canvas.FocusNext() + } + case fyne.KeyDown: + i.Child().ActivateNext() + case fyne.KeyUp: + i.Child().ActivatePrevious() + case fyne.KeyEnter, fyne.KeyReturn, fyne.KeySpace: + i.Child().TriggerLast() + } +} + +func (i *menuBarItem) TypedRune(_ rune) { +} + +type menuBarItemRenderer struct { + widget.BaseRenderer + i *menuBarItem + text *canvas.Text + background *canvas.Rectangle +} + +func (r *menuBarItemRenderer) Layout(size fyne.Size) { + padding := r.padding() + + r.text.TextSize = theme.TextSize() + r.text.Color = theme.Color(theme.ColorNameForeground) + r.text.Resize(r.text.MinSize()) + r.text.Move(fyne.NewPos(padding.Width/2, padding.Height/2)) + + r.background.Resize(size) +} + +func (r *menuBarItemRenderer) MinSize() fyne.Size { + return r.text.MinSize().Add(r.padding()) +} + +func (r *menuBarItemRenderer) Refresh() { + r.background.CornerRadius = theme.SelectionRadiusSize() + if r.i.active && r.i.Parent.active { + r.background.FillColor = theme.Color(theme.ColorNameFocus) + r.background.Show() + } else if r.i.hovered && !r.i.Parent.active { + r.background.FillColor = theme.Color(theme.ColorNameHover) + r.background.Show() + } else { + r.background.Hide() + } + r.background.Refresh() + canvas.Refresh(r.i) +} + +func (r *menuBarItemRenderer) padding() fyne.Size { + return fyne.NewSize(theme.InnerPadding()*2, theme.InnerPadding()) +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/menu_darwin.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/menu_darwin.go new file mode 100644 index 0000000..e795be2 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/menu_darwin.go @@ -0,0 +1,354 @@ +//go:build darwin && !no_native_menus + +package glfw + +import ( + "bytes" + "fmt" + "image/color" + "image/png" + "strings" + "unsafe" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/internal/painter" + "fyne.io/fyne/v2/internal/svg" + "fyne.io/fyne/v2/theme" +) + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Foundation -framework AppKit + +#include + +// Using void* as type for pointers is a workaround. See https://github.com/golang/go/issues/12065. +void assignDarwinSubmenu(const void*, const void*); +void completeDarwinMenu(void* menu, bool prepend); +const void* createDarwinMenu(const char* label); +const void* darwinAppMenu(); +void getTextColorRGBA(int* r, int* g, int* b, int* a); +const void* insertDarwinMenuItem(const void* menu, const char* label, const char* keyEquivalent, unsigned int keyEquivalentModifierMask, int id, int index, bool isSeparator, const void *imageData, unsigned int imageDataLength); +int menuFontSize(); +void resetDarwinMenu(); + +// Used for tests. +const void* test_darwinMainMenu(); +const void* test_NSMenu_itemAtIndex(const void*, NSInteger); +NSInteger test_NSMenu_numberOfItems(const void*); +void test_NSMenu_performActionForItemAtIndex(const void*, NSInteger); +void test_NSMenu_removeItemAtIndex(const void* m, NSInteger i); +const char* test_NSMenu_title(const void*); +bool test_NSMenuItem_isSeparatorItem(const void*); +const char* test_NSMenuItem_keyEquivalent(const void*); +unsigned long test_NSMenuItem_keyEquivalentModifierMask(const void*); +const void* test_NSMenuItem_submenu(const void*); +const char* test_NSMenuItem_title(const void*); +*/ +import "C" + +type menuCallbacks struct { + action func() + enabled func() bool + checked func() bool +} + +var ( + callbacks []*menuCallbacks + ecb func(string) + specialKeys = map[fyne.KeyName]string{ + fyne.KeyBackspace: "\x08", + fyne.KeyDelete: "\x7f", + fyne.KeyDown: "\uf701", + fyne.KeyEnd: "\uf72b", + fyne.KeyEnter: "\x03", + fyne.KeyEscape: "\x1b", + fyne.KeyF10: "\uf70d", + fyne.KeyF11: "\uf70e", + fyne.KeyF12: "\uf70f", + fyne.KeyF1: "\uf704", + fyne.KeyF2: "\uf705", + fyne.KeyF3: "\uf706", + fyne.KeyF4: "\uf707", + fyne.KeyF5: "\uf708", + fyne.KeyF6: "\uf709", + fyne.KeyF7: "\uf70a", + fyne.KeyF8: "\uf70b", + fyne.KeyF9: "\uf70c", + fyne.KeyHome: "\uf729", + fyne.KeyInsert: "\uf727", + fyne.KeyLeft: "\uf702", + fyne.KeyPageDown: "\uf72d", + fyne.KeyPageUp: "\uf72c", + fyne.KeyReturn: "\n", + fyne.KeyRight: "\uf703", + fyne.KeySpace: " ", + fyne.KeyTab: "\t", + fyne.KeyUp: "\uf700", + } +) + +func addNativeMenu(w *window, menu *fyne.Menu, nextItemID int, prepend bool) int { + menu, nextItemID = handleSpecialItems(w, menu, nextItemID, true) + + containsItems := false + for _, item := range menu.Items { + if !item.IsSeparator { + containsItems = true + break + } + } + if !containsItems { + return nextItemID + } + + nsMenu, nextItemID := createNativeMenu(w, menu, nextItemID) + C.completeDarwinMenu(nsMenu, C.bool(prepend)) + return nextItemID +} + +func addNativeSubmenu(w *window, nsParentMenuItem unsafe.Pointer, menu *fyne.Menu, nextItemID int) int { + nsMenu, nextItemID := createNativeMenu(w, menu, nextItemID) + C.assignDarwinSubmenu(nsParentMenuItem, nsMenu) + return nextItemID +} + +func clearNativeMenu() { + C.resetDarwinMenu() +} + +func createNativeMenu(w *window, menu *fyne.Menu, nextItemID int) (unsafe.Pointer, int) { + nsMenu := C.createDarwinMenu(C.CString(menu.Label)) + for _, item := range menu.Items { + nsMenuItem := insertNativeMenuItem(nsMenu, item, nextItemID, -1) + nextItemID = registerCallback(w, item, nextItemID) + if item.ChildMenu != nil { + nextItemID = addNativeSubmenu(w, nsMenuItem, item.ChildMenu, nextItemID) + } + } + return nsMenu, nextItemID +} + +//export exceptionCallback +func exceptionCallback(e *C.char) { + msg := C.GoString(e) + if ecb == nil { + panic("unhandled Obj-C exception: " + msg) + } + ecb(msg) +} + +func handleSpecialItems(w *window, menu *fyne.Menu, nextItemID int, addSeparator bool) (*fyne.Menu, int) { + menu = fyne.NewMenu(menu.Label, menu.Items...) // copy so we can manipulate + for i := 0; i < len(menu.Items); i++ { + item := menu.Items[i] + switch item.Label { + case "About", "Settings", "Settings…", "Preferences", "Preferences…": + items := make([]*fyne.MenuItem, 0, len(menu.Items)-1) + items = append(items, menu.Items[:i]...) + items = append(items, menu.Items[i+1:]...) + menu, nextItemID = handleSpecialItems(w, fyne.NewMenu(menu.Label, items...), nextItemID, false) + i-- + + insertNativeMenuItem(C.darwinAppMenu(), item, nextItemID, 1) + if addSeparator && item.Label != "About" { + C.insertDarwinMenuItem( + C.darwinAppMenu(), + C.CString(""), + C.CString(""), + C.uint(0), + C.int(nextItemID), + C.int(1), + C.bool(true), + unsafe.Pointer(nil), + C.uint(0), + ) + } + nextItemID = registerCallback(w, item, nextItemID) + } + } + return menu, nextItemID +} + +// TODO: theme change support, see NSSystemColorsDidChangeNotification +func insertNativeMenuItem(nsMenu unsafe.Pointer, item *fyne.MenuItem, nextItemID, index int) unsafe.Pointer { + var imgData unsafe.Pointer + var imgDataLength uint + if item.Icon != nil { + if svg.IsResourceSVG(item.Icon) { + rsc := item.Icon + if _, isThemed := rsc.(*theme.ThemedResource); isThemed { + var r, g, b, a C.int + C.getTextColorRGBA(&r, &g, &b, &a) + content, err := svg.Colorize(rsc.Content(), color.NRGBA{R: uint8(r), G: uint8(g), B: uint8(b), A: uint8(a)}) + if err != nil { + fyne.LogError("", err) + } + rsc = &fyne.StaticResource{ + StaticName: rsc.Name(), + StaticContent: content, + } + } + size := int(C.menuFontSize()) + img := painter.PaintImage(&canvas.Image{Resource: rsc}, nil, size, size) + var buf bytes.Buffer + if err := png.Encode(&buf, img); err != nil { + fyne.LogError("failed to render menu icon", err) + } else { + imgData = unsafe.Pointer(&buf.Bytes()[0]) + imgDataLength = uint(buf.Len()) + } + } else { + imgData = unsafe.Pointer(&item.Icon.Content()[0]) + imgDataLength = uint(len(item.Icon.Content())) + } + } + return C.insertDarwinMenuItem( + nsMenu, + C.CString(item.Label), + C.CString(keyEquivalent(item)), + C.uint(keyEquivalentModifierMask(item)), + C.int(nextItemID), + C.int(index), + C.bool(item.IsSeparator), + imgData, + C.uint(imgDataLength), + ) +} + +func keyEquivalent(item *fyne.MenuItem) (key string) { + if s, ok := item.Shortcut.(fyne.KeyboardShortcut); ok { + if key = specialKeys[s.Key()]; key == "" { + if len(s.Key()) > 1 { + fyne.LogError(fmt.Sprintf("unsupported key “%s” for menu shortcut", s.Key()), nil) + } + key = strings.ToLower(string(s.Key())) + } + } + return key +} + +func keyEquivalentModifierMask(item *fyne.MenuItem) (mask uint) { + if s, ok := item.Shortcut.(fyne.KeyboardShortcut); ok { + if (s.Mod() & fyne.KeyModifierShift) != 0 { + mask |= 1 << 17 // NSEventModifierFlagShift + } + if (s.Mod() & fyne.KeyModifierAlt) != 0 { + mask |= 1 << 19 // NSEventModifierFlagOption + } + if (s.Mod() & fyne.KeyModifierControl) != 0 { + mask |= 1 << 18 // NSEventModifierFlagControl + } + if (s.Mod() & fyne.KeyModifierSuper) != 0 { + mask |= 1 << 20 // NSEventModifierFlagCommand + } + } + return mask +} + +func registerCallback(w *window, item *fyne.MenuItem, nextItemID int) int { + if !item.IsSeparator { + callbacks = append(callbacks, &menuCallbacks{ + action: func() { + if item.Action != nil { + item.Action() + } + }, + enabled: func() bool { + return !item.Disabled + }, + checked: func() bool { + return item.Checked + }, + }) + nextItemID++ + } + return nextItemID +} + +func setExceptionCallback(cb func(string)) { + ecb = cb +} + +//export menuCallback +func menuCallback(id int) { + callbacks[id].action() +} + +//export menuEnabled +func menuEnabled(id int) bool { + return callbacks[id].enabled() +} + +//export menuChecked +func menuChecked(id int) bool { + return callbacks[id].checked() +} + +func setupNativeMenu(w *window, main *fyne.MainMenu) { + clearNativeMenu() + nextItemID := 0 + callbacks = []*menuCallbacks{} + var helpMenu *fyne.Menu + for i := len(main.Items) - 1; i >= 0; i-- { + menu := main.Items[i] + if menu.Label == "Help" { + helpMenu = menu + continue + } + nextItemID = addNativeMenu(w, menu, nextItemID, true) + } + if helpMenu != nil { + addNativeMenu(w, helpMenu, nextItemID, false) + } +} + +// +// Test support methods +// These are needed because CGo is not supported inside test files. +// + +func testDarwinMainMenu() unsafe.Pointer { + return C.test_darwinMainMenu() +} + +func testNSMenuItemAtIndex(m unsafe.Pointer, i int) unsafe.Pointer { + return C.test_NSMenu_itemAtIndex(m, C.long(i)) +} + +func testNSMenuNumberOfItems(m unsafe.Pointer) int { + return int(C.test_NSMenu_numberOfItems(m)) +} + +func testNSMenuPerformActionForItemAtIndex(m unsafe.Pointer, i int) { + C.test_NSMenu_performActionForItemAtIndex(m, C.long(i)) +} + +func testNSMenuRemoveItemAtIndex(m unsafe.Pointer, i int) { + C.test_NSMenu_removeItemAtIndex(m, C.long(i)) +} + +func testNSMenuTitle(m unsafe.Pointer) string { + return C.GoString(C.test_NSMenu_title(m)) +} + +func testNSMenuItemIsSeparatorItem(i unsafe.Pointer) bool { + return bool(C.test_NSMenuItem_isSeparatorItem(i)) +} + +func testNSMenuItemKeyEquivalent(i unsafe.Pointer) string { + return C.GoString(C.test_NSMenuItem_keyEquivalent(i)) +} + +func testNSMenuItemKeyEquivalentModifierMask(i unsafe.Pointer) uint64 { + return uint64(C.ulong(C.test_NSMenuItem_keyEquivalentModifierMask(i))) +} + +func testNSMenuItemSubmenu(i unsafe.Pointer) unsafe.Pointer { + return C.test_NSMenuItem_submenu(i) +} + +func testNSMenuItemTitle(i unsafe.Pointer) string { + return C.GoString(C.test_NSMenuItem_title(i)) +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/menu_darwin.m b/vendor/fyne.io/fyne/v2/internal/driver/glfw/menu_darwin.m new file mode 100644 index 0000000..b40be98 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/menu_darwin.m @@ -0,0 +1,236 @@ +//go:build !no_native_menus + +#import +#import + +const int menuTagMin = 5000; + +#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101400 +NSControlStateValue STATE_ON = NSControlStateValueOn; +NSControlStateValue STATE_OFF = NSControlStateValueOff; +#else +NSCellStateValue STATE_ON = NSOnState; +NSCellStateValue STATE_OFF = NSOffState; +#endif + + +extern void menuCallback(int); +extern BOOL menuEnabled(int); +extern BOOL menuChecked(int); +extern void exceptionCallback(const char*); + +@interface FyneMenuHandler : NSObject { +} +@end + +@implementation FyneMenuHandler ++ (void) tapped:(NSMenuItem*) item { + menuCallback([item tag]-menuTagMin); +} ++ (BOOL) validateMenuItem:(NSMenuItem*) item { + BOOL checked = menuChecked([item tag]-menuTagMin); + if (checked) { + [item setState:STATE_ON]; + } else { + [item setState:STATE_OFF]; + } + + return menuEnabled([item tag]-menuTagMin); +} +@end + +// forward declaration … we want methods to be ordered alphabetically +NSMenu* nativeMainMenu(); + +void assignDarwinSubmenu(const void* i, const void* m) { + NSMenu* menu = (NSMenu*)m; // this menu is created in the createDarwinMenu() function + NSMenuItem *item = (NSMenuItem*)i; + [item setSubmenu:menu]; // this retains the menu + [menu release]; // release the menu +} + +void completeDarwinMenu(const void* m, bool prepend) { + NSMenu* main = nativeMainMenu(); + NSMenuItem* top = [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""]; + [top setTag:menuTagMin]; + if (prepend) { + [main insertItem:top atIndex:1]; + } else { + [main addItem:top]; + } + assignDarwinSubmenu(top, m); +} + +const void* createDarwinMenu(const char* label) { + return (void*)[[NSMenu alloc] initWithTitle:[NSString stringWithUTF8String:label]]; +} + +const void* darwinAppMenu() { + return [[nativeMainMenu() itemAtIndex:0] submenu]; +} + +void getTextColorRGBA(int* r, int* g, int* b, int* a) { + CGFloat fr, fg, fb, fa; + NSColor *c = [[NSColor selectedMenuItemTextColor] colorUsingColorSpace: [NSColorSpace sRGBColorSpace]]; + [c getRed: &fr green: &fg blue: &fb alpha: &fa]; + *r = fr*255.0; + *g = fg*255.0; + *b = fb*255.0; + *a = fa*255.0; +} + +void handleException(const char* m, id e) { + exceptionCallback([[NSString stringWithFormat:@"%s failed: %@", m, e] UTF8String]); +} + +int replacedAbout = 0; + +const void* insertDarwinMenuItem(const void* m, const char* label, const char* keyEquivalent, unsigned int keyEquivalentModifierMask, int nextId, int index, bool isSeparator, const void *imageData, unsigned int imageDataLength) { + NSMenu* menu = (NSMenu*)m; + NSMenuItem* item; + + if (strcmp(label, "About") == 0 && !replacedAbout) { + replacedAbout = 1; + item = [menu itemArray][0]; + [item setAction:@selector(tapped:)]; + [item setTarget:[FyneMenuHandler class]]; + [item setTag:nextId+menuTagMin]; + return item; + } + + if (isSeparator) { + item = [NSMenuItem separatorItem]; + } else { + item = [[NSMenuItem alloc] + initWithTitle:[NSString stringWithUTF8String:label] + action:@selector(tapped:) + keyEquivalent:[NSString stringWithUTF8String:keyEquivalent]]; + if (keyEquivalentModifierMask) { + [item setKeyEquivalentModifierMask: keyEquivalentModifierMask]; + } + [item setTarget:[FyneMenuHandler class]]; + [item setTag:nextId+menuTagMin]; + if (imageData) { + char *x = (char *)imageData; + NSData *data = [[NSData alloc] initWithBytes: imageData length: imageDataLength]; + NSImage *image = [[NSImage alloc] initWithData: data]; + [item setImage: image]; + [data release]; + [image release]; + } + } + + if (index > -1) { + [menu insertItem:item atIndex:index]; + } else { + [menu addItem:item]; + } + [item release]; // retained by the menu + return item; +} + +int menuFontSize() { + return ceil([[NSFont menuFontOfSize: 0] pointSize]); +} + +NSMenu* nativeMainMenu() { + NSApplication* app = [NSApplication sharedApplication]; + return [app mainMenu]; +} + +void resetDarwinMenu() { + NSMenu *root = nativeMainMenu(); + NSEnumerator *items = [[root itemArray] objectEnumerator]; + + id object; + while (object = [items nextObject]) { + NSMenuItem *item = object; + if ([item tag] < menuTagMin) { + // check for inserted items (like Settings...) + NSMenu *menu = [item submenu]; + NSEnumerator *subItems = [[menu itemArray] objectEnumerator]; + + id sub; + while (sub = [subItems nextObject]) { + NSMenuItem *item = sub; + if ([item tag] >= menuTagMin) { + [menu removeItem: item]; + } + } + + continue; + } + [root removeItem: item]; + } +} + +const void* test_darwinMainMenu() { + return nativeMainMenu(); +} + +const void* test_NSMenu_itemAtIndex(const void* m, NSInteger i) { + NSMenu* menu = (NSMenu*)m; + @try { + return [menu itemAtIndex: i]; + } @catch(NSException* e) { + handleException("test_NSMenu_itemAtIndex", e); + return NULL; + } +} + +NSInteger test_NSMenu_numberOfItems(const void* m) { + NSMenu* menu = (NSMenu*)m; + return [menu numberOfItems]; +} + +void test_NSMenu_performActionForItemAtIndex(const void* m, NSInteger i) { + NSMenu* menu = (NSMenu*)m; + @try { + // Using performActionForItemAtIndex: would be better but sadly it crashes. + // We simulate the relevant effect for now. + // [menu performActionForItemAtIndex:i]; + NSMenuItem* item = [menu itemAtIndex:i]; + [[item target] performSelector:[item action] withObject:item]; + } @catch(NSException* e) { + handleException("test_NSMenu_performActionForItemAtIndex", e); + } +} + +void test_NSMenu_removeItemAtIndex(const void* m, NSInteger i) { + NSMenu* menu = (NSMenu*)m; + @try { + [menu removeItemAtIndex: i]; + } @catch(NSException* e) { + handleException("test_NSMenu_removeItemAtIndex", e); + } +} + +const char* test_NSMenu_title(const void* m) { + NSMenu* menu = (NSMenu*)m; + return [[menu title] UTF8String]; +} + +bool test_NSMenuItem_isSeparatorItem(const void* i) { + NSMenuItem* item = (NSMenuItem*)i; + return [item isSeparatorItem]; +} + +const char* test_NSMenuItem_keyEquivalent(const void *i) { + NSMenuItem* item = (NSMenuItem*)i; + return [[item keyEquivalent] UTF8String]; +} + +unsigned long test_NSMenuItem_keyEquivalentModifierMask(const void *i) { + NSMenuItem* item = (NSMenuItem*)i; + return [item keyEquivalentModifierMask]; +} + +const void* test_NSMenuItem_submenu(const void* i) { + NSMenuItem* item = (NSMenuItem*)i; + return [item submenu]; +} + +const char* test_NSMenuItem_title(const void* i) { + NSMenuItem* item = (NSMenuItem*)i; + return [[item title] UTF8String]; +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/menu_notweb.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/menu_notweb.go new file mode 100644 index 0000000..6802cfe --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/menu_notweb.go @@ -0,0 +1,38 @@ +//go:build !wasm && !test_web_driver + +package glfw + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/lang" +) + +func addMissingQuitForMainMenu(menus *fyne.MainMenu, w *window) *fyne.MainMenu { + localQuit := lang.L("Quit") + var lastItem *fyne.MenuItem + if len(menus.Items[0].Items) > 0 { + lastItem = menus.Items[0].Items[len(menus.Items[0].Items)-1] + if lastItem.Label == localQuit { + lastItem.IsQuit = true + } + } + if lastItem == nil || !lastItem.IsQuit { // make sure the first menu always has a quit option + quitItem := fyne.NewMenuItem(localQuit, nil) + quitItem.IsQuit = true + menus.Items[0].Items = append(menus.Items[0].Items, fyne.NewMenuItemSeparator(), quitItem) + } + for _, item := range menus.Items[0].Items { + if item.IsQuit && item.Action == nil { + item.Action = func() { + for _, win := range w.driver.AllWindows() { + if glWin, ok := win.(*window); ok { + glWin.closed(glWin.view()) + } else { + win.Close() // for test windows + } + } + } + } + } + return menus +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/menu_other.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/menu_other.go new file mode 100644 index 0000000..b26ce98 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/menu_other.go @@ -0,0 +1,9 @@ +//go:build !darwin || no_native_menus + +package glfw + +import "fyne.io/fyne/v2" + +func setupNativeMenu(_ *window, _ *fyne.MainMenu) { + // no-op +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/menu_wasm.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/menu_wasm.go new file mode 100644 index 0000000..215937b --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/menu_wasm.go @@ -0,0 +1,10 @@ +//go:build wasm || test_web_driver + +package glfw + +import "fyne.io/fyne/v2" + +func addMissingQuitForMainMenu(menus *fyne.MainMenu, w *window) *fyne.MainMenu { + // no-op for a web browser + return menus +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/scale.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/scale.go new file mode 100644 index 0000000..90eb62c --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/scale.go @@ -0,0 +1,61 @@ +package glfw + +import ( + "math" + "os" + "strconv" + + "fyne.io/fyne/v2" +) + +const ( + baselineDPI = 120.0 + scaleEnvKey = "FYNE_SCALE" + scaleAuto = float32(-1.0) // some platforms allow setting auto-scale (linux/BSD) +) + +func calculateDetectedScale(widthMm, widthPx int) float32 { + dpi := float32(widthPx) / (float32(widthMm) / 25.4) + if dpi > 1000 || dpi < 10 { + dpi = baselineDPI + } + + scale := float32(float64(dpi) / baselineDPI) + if scale < 1.0 { + return 1.0 + } + return scale +} + +func calculateScale(user, system, detected float32) float32 { + if user < 0 { + user = 1.0 + } + + if system == scaleAuto { + system = detected + } + + raw := system * user + return float32(math.Round(float64(raw*10.0))) / 10.0 +} + +func userScale() float32 { + env := os.Getenv(scaleEnvKey) + + if env != "" && env != "auto" { + scale, err := strconv.ParseFloat(env, 32) + if err == nil && scale != 0 { + return float32(scale) + } + fyne.LogError("Error reading scale", err) + } + + if env != "auto" { + if setting := fyne.CurrentApp().Settings().Scale(); setting > 0 { + return setting + } + } + + return 1.0 // user preference for auto is now passed as 1 so the system auto is picked up +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/scroll_speed_darwin.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/scroll_speed_darwin.go new file mode 100644 index 0000000..dcc985d --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/scroll_speed_darwin.go @@ -0,0 +1,11 @@ +//go:build darwin + +package glfw + +const ( + // MacOS applies its own scroll accelerate curve, so set + // scrollAccelerateRate to 1 for no acceleration effect + scrollAccelerateRate = float64(1) + scrollAccelerateCutoff = float64(5) + scrollSpeed = float32(10) +) diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/scroll_speed_default.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/scroll_speed_default.go new file mode 100644 index 0000000..c101868 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/scroll_speed_default.go @@ -0,0 +1,9 @@ +//go:build !darwin && !wasm && !test_web_driver + +package glfw + +const ( + scrollAccelerateRate = float64(125) + scrollAccelerateCutoff = float64(10) + scrollSpeed = float32(25) +) diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/scroll_speed_wasm.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/scroll_speed_wasm.go new file mode 100644 index 0000000..1a73c07 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/scroll_speed_wasm.go @@ -0,0 +1,9 @@ +//go:build wasm || test_web_driver + +package glfw + +const ( + scrollAccelerateRate = float64(10) + scrollAccelerateCutoff = float64(5) + scrollSpeed = float32(0.2) +) diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/window.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/window.go new file mode 100644 index 0000000..de4b12d --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/window.go @@ -0,0 +1,999 @@ +package glfw + +import ( + "context" + "image/color" + _ "image/png" // for the icon + "math" + "runtime" + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/driver/desktop" + "fyne.io/fyne/v2/internal/app" + "fyne.io/fyne/v2/internal/async" + "fyne.io/fyne/v2/internal/build" + "fyne.io/fyne/v2/internal/cache" + "fyne.io/fyne/v2/internal/driver" + "fyne.io/fyne/v2/internal/driver/common" + "fyne.io/fyne/v2/internal/scale" +) + +const ( + dragMoveThreshold = 2 // how far can we move before it is a drag + windowIconSize = 256 +) + +func (w *window) Title() string { + return w.title +} + +func (w *window) SetTitle(title string) { + w.title = title + + w.runOnMainWhenCreated(func() { + w.view().SetTitle(title) + }) +} + +func (w *window) FullScreen() bool { + return w.fullScreen +} + +// minSizeOnScreen gets the padded minimum size of a window content in screen pixels +func (w *window) minSizeOnScreen() (int, int) { + // get minimum size of content inside the window + return w.screenSize(w.canvas.MinSize()) +} + +// screenSize computes the actual output size of the given content size in screen pixels +func (w *window) screenSize(canvasSize fyne.Size) (int, int) { + return scale.ToScreenCoordinate(w.canvas, canvasSize.Width), scale.ToScreenCoordinate(w.canvas, canvasSize.Height) +} + +func (w *window) Resize(size fyne.Size) { + w.canvas.size = size + // we cannot perform this until window is prepared as we don't know its scale! + bigEnough := size.Max(w.canvas.canvasSize(w.canvas.Content().MinSize())) + w.runOnMainWhenCreated(func() { + width, height := scale.ToScreenCoordinate(w.canvas, bigEnough.Width), scale.ToScreenCoordinate(w.canvas, bigEnough.Height) + if w.fixedSize || !w.visible { // fixed size ignores future `resized` and if not visible we may not get the event + w.shouldWidth, w.shouldHeight = width, height + w.width, w.height = width, height + } + + w.requestedWidth, w.requestedHeight = width, height + if runtime.GOOS != "js" { + w.view().SetSize(width, height) + w.processResized(width, height) + } + }) +} + +func (w *window) FixedSize() bool { + return w.fixedSize +} + +func (w *window) SetFixedSize(fixed bool) { + w.fixedSize = fixed + w.runOnMainWhenCreated(func() { + w.fitContent() + if !w.centered { + w.processResized(w.width, w.height) + } + }) +} + +func (w *window) Padded() bool { + return w.canvas.padded +} + +func (w *window) SetPadded(padded bool) { + w.canvas.SetPadded(padded) + + w.runOnMainWhenCreated(w.fitContent) +} + +func (w *window) Icon() fyne.Resource { + if w.icon == nil { + return fyne.CurrentApp().Icon() + } + + return w.icon +} + +func (w *window) MainMenu() *fyne.MainMenu { + return w.mainmenu +} + +func (w *window) SetMainMenu(menu *fyne.MainMenu) { + w.mainmenu = menu + w.runOnMainWhenCreated(func() { + w.canvas.buildMenu(w, menu) + }) +} + +func (w *window) SetOnClosed(closed func()) { + w.onClosed = closed +} + +func (w *window) SetCloseIntercept(callback func()) { + w.onCloseIntercepted = callback +} + +func (w *window) calculatedScale() float32 { + return calculateScale(userScale(), fyne.CurrentDevice().SystemScaleForWindow(w), w.detectScale()) +} + +func (w *window) detectTextureScale() float32 { + view := w.view() + winWidth, _ := view.GetSize() + texWidth, _ := view.GetFramebufferSize() + return float32(texWidth) / float32(winWidth) +} + +func (w *window) Show() { + async.EnsureMain(func() { + if w.view() != nil { + w.doShowAgain() + return + } + + if !w.created { + w.created = true + w.create() + } + + if w.view() == nil { + return + } + + w.visible = true + view := w.view() + view.SetTitle(w.title) + + if !build.IsWayland && w.centered { + w.doCenterOnScreen() // lastly center if that was requested + } + view.Show() + + // save coordinates + if !build.IsWayland { + w.xpos, w.ypos = view.GetPos() + } + + if w.fullScreen { // this does not work if called before viewport.Show() + w.doSetFullScreen(true) + } + + // show top canvas element + if content := w.canvas.Content(); content != nil { + w.RunWithContext(func() { + w.driver.repaintWindow(w) + }) + } + }) +} + +func (w *window) Hide() { + async.EnsureMain(func() { + if w.closing || w.viewport == nil { + return + } + + w.visible = false + w.viewport.Hide() + }) +} + +func (w *window) Close() { + async.EnsureMain(func() { + if w.isClosing() { + return + } + + // trigger callbacks - early so window still exists + if fn := w.onClosed; fn != nil { + w.onClosed = nil // avoid possibility of calling twice + fn() + } + + w.closing = true + w.viewport.SetShouldClose(true) + + cache.RangeTexturesFor(w.canvas, w.canvas.Painter().Free) + w.canvas.WalkTrees(nil, func(node *common.RenderCacheNode, _ fyne.Position) { + if wid, ok := node.Obj().(fyne.Widget); ok { + cache.DestroyRenderer(wid) + } + }) + }) +} + +func (w *window) ShowAndRun() { + w.Show() + fyne.CurrentApp().Run() +} + +// Clipboard returns the system clipboard +func (w *window) Clipboard() fyne.Clipboard { + return NewClipboard() +} + +func (w *window) Content() fyne.CanvasObject { + return w.canvas.Content() +} + +func (w *window) SetContent(content fyne.CanvasObject) { + w.canvas.SetContent(content) + + async.EnsureMain(func() { + w.RunWithContext(w.RescaleContext) + }) +} + +func (w *window) Canvas() fyne.Canvas { + return w.canvas +} + +func (w *window) processClosed() { + if w.onCloseIntercepted != nil { + w.onCloseIntercepted() + return + } + + w.Close() +} + +// destroy this window and, if it's the last window quit the app +func (w *window) destroy(d *gLDriver) { + cache.CleanCanvas(w.canvas) + + if w.master { + d.Quit() + } else if runtime.GOOS == "darwin" { + d.focusPreviousWindow() + } +} + +func (w *window) drainPendingEvents() { + for _, fn := range w.pending { + fn() + } + w.pending = nil +} + +func (w *window) processMoved(x, y int) { + if !w.fullScreen { // don't save the move to top left when changing to fullscreen + // save coordinates + w.xpos, w.ypos = x, y + } + + if w.canvas.detectedScale == w.detectScale() { + return + } + + w.canvas.detectedScale = w.detectScale() + w.canvas.reloadScale() +} + +func (w *window) processResized(width, height int) { + canvasSize := w.computeCanvasSize(width, height) + if !w.fullScreen { + w.width = scale.ToScreenCoordinate(w.canvas, canvasSize.Width) + w.height = scale.ToScreenCoordinate(w.canvas, canvasSize.Height) + } + + if !w.visible { // don't redraw if hidden + w.canvas.Resize(canvasSize) + return + } + + if w.fixedSize { + w.canvas.Resize(canvasSize) + w.fitContent() + return + } + + w.RunWithContext(func() { + w.platformResize(canvasSize) + }) +} + +func (w *window) processFrameSized(width, height int) { + if width == 0 || height == 0 || runtime.GOOS != "darwin" { + return + } + + winWidth, _ := w.view().GetSize() + newTexScale := float32(width) / float32(winWidth) // This will be > 1.0 on a HiDPI screen + if w.canvas.texScale != newTexScale { + w.canvas.texScale = newTexScale + w.canvas.Refresh(w.canvas.Content()) // reset graphics to apply texture scale + } +} + +func (w *window) processRefresh() { + refreshWindow(w) +} + +func (w *window) findObjectAtPositionMatching(canvas *glCanvas, mouse fyne.Position, matches func(object fyne.CanvasObject) bool) (fyne.CanvasObject, fyne.Position, int) { + return driver.FindObjectAtPositionMatching(mouse, matches, canvas.Overlays().Top(), canvas.menu, canvas.Content()) +} + +func (w *window) processMouseMoved(xpos float64, ypos float64) { + previousPos := w.mousePos + w.mousePos = fyne.NewPos(scale.ToFyneCoordinate(w.canvas, int(xpos)), scale.ToFyneCoordinate(w.canvas, int(ypos))) + mousePos := w.mousePos + mouseButton := w.mouseButton + mouseDragPos := w.mouseDragPos + mouseOver := w.mouseOver + + cursor := desktop.Cursor(desktop.DefaultCursor) + + obj, pos, _ := w.findObjectAtPositionMatching(w.canvas, mousePos, func(object fyne.CanvasObject) bool { + if cursorable, ok := object.(desktop.Cursorable); ok { + cursor = cursorable.Cursor() + } + + _, hover := object.(desktop.Hoverable) + return hover + }) + + if w.cursor != cursor { + // cursor has changed, store new cursor and apply change via glfw + rawCursor, isCustomCursor := fyneToNativeCursor(cursor) + w.cursor = cursor + + if rawCursor == nil { + w.view().SetInputMode(CursorMode, CursorHidden) + } else { + w.view().SetInputMode(CursorMode, CursorNormal) + w.SetCursor(rawCursor) + } + w.setCustomCursor(rawCursor, isCustomCursor) + } + + if w.mouseButton != 0 && w.mouseButton != desktop.MouseButtonSecondary && !w.mouseDragStarted { + obj, pos, _ := w.findObjectAtPositionMatching(w.canvas, previousPos, func(object fyne.CanvasObject) bool { + _, ok := object.(fyne.Draggable) + return ok + }) + + deltaX := mousePos.X - mouseDragPos.X + deltaY := mousePos.Y - mouseDragPos.Y + overThreshold := math.Abs(float64(deltaX)) >= dragMoveThreshold || math.Abs(float64(deltaY)) >= dragMoveThreshold + + if wid, ok := obj.(fyne.Draggable); ok && overThreshold { + w.mouseDragged = wid + w.mouseDraggedOffset = previousPos.Subtract(pos) + w.mouseDraggedObjStart = obj.Position() + w.mouseDragStarted = true + } + } + + if obj != nil && !w.objIsDragged(obj) { + ev := &desktop.MouseEvent{Button: mouseButton} + ev.AbsolutePosition = mousePos + ev.Position = pos + + if hovered, ok := obj.(desktop.Hoverable); ok { + if hovered == mouseOver { + hovered.MouseMoved(ev) + } else { + w.mouseOut() + w.mouseIn(hovered, ev) + } + } else if mouseOver != nil { + isChild := false + driver.WalkCompleteObjectTree(mouseOver.(fyne.CanvasObject), + func(co fyne.CanvasObject, p1, p2 fyne.Position, s fyne.Size) bool { + if co == obj { + isChild = true + return true + } + return false + }, nil) + if !isChild { + w.mouseOut() + } + } + } else if mouseOver != nil && !w.objIsDragged(mouseOver) { + w.mouseOut() + } + + mouseDragged := w.mouseDragged + mouseDragPos = w.mouseDragPos + if mouseDragged != nil && w.mouseButton != desktop.MouseButtonSecondary { + if w.mouseButton > 0 { + draggedObjDelta := w.mouseDraggedObjStart.Subtract(mouseDragged.(fyne.CanvasObject).Position()) + ev := &fyne.DragEvent{} + ev.AbsolutePosition = mousePos + ev.Position = mousePos.Subtract(w.mouseDraggedOffset).Add(draggedObjDelta) + ev.Dragged = fyne.NewDelta(mousePos.X-mouseDragPos.X, mousePos.Y-mouseDragPos.Y) + wd := mouseDragged + wd.Dragged(ev) + } + + w.mouseDragStarted = true + w.mouseDragPos = mousePos + } +} + +func (w *window) objIsDragged(obj any) bool { + if w.mouseDragged != nil && obj != nil { + draggedObj, _ := obj.(fyne.Draggable) + return draggedObj == w.mouseDragged + } + return false +} + +func (w *window) mouseIn(obj desktop.Hoverable, ev *desktop.MouseEvent) { + if obj != nil { + obj.MouseIn(ev) + } + w.mouseOver = obj +} + +func (w *window) mouseOut() { + mouseOver := w.mouseOver + if mouseOver != nil { + mouseOver.MouseOut() + w.mouseOver = nil + } +} + +func (w *window) processMouseClicked(button desktop.MouseButton, action action, modifiers fyne.KeyModifier) { + w.mouseDragPos = w.mousePos + mousePos := w.mousePos + mouseDragStarted := w.mouseDragStarted + if mousePos.IsZero() { // window may not be focused (darwin mostly) and so position callbacks not happening + xpos, ypos := w.view().GetCursorPos() + w.mousePos = fyne.NewPos(scale.ToFyneCoordinate(w.canvas, int(xpos)), scale.ToFyneCoordinate(w.canvas, int(ypos))) + mousePos = w.mousePos + } + + co, pos, _ := w.findObjectAtPositionMatching(w.canvas, mousePos, func(object fyne.CanvasObject) bool { + switch object.(type) { + case fyne.Tappable, fyne.SecondaryTappable, fyne.DoubleTappable, fyne.Focusable, desktop.Mouseable: + return true + case fyne.Draggable: + if mouseDragStarted { + return true + } + } + + return false + }) + ev := &fyne.PointEvent{ + Position: pos, + AbsolutePosition: mousePos, + } + + coMouse := co + if wid, ok := co.(desktop.Mouseable); ok { + mev := &desktop.MouseEvent{ + Button: button, + Modifier: modifiers, + } + mev.Position = ev.Position + mev.AbsolutePosition = mousePos + w.mouseClickedHandleMouseable(mev, action, wid) + } + + if wid, ok := co.(fyne.Focusable); !ok || wid != w.canvas.Focused() { + ignore := false + _, _, _ = w.findObjectAtPositionMatching(w.canvas, mousePos, func(object fyne.CanvasObject) bool { + switch object.(type) { + case fyne.Focusable: + ignore = true + return true + } + + return false + }) + + if !ignore { // if a parent item under the mouse has focus then ignore this tap unfocus + w.canvas.Unfocus() + } + } + + switch action { + case press: + w.mouseButton |= button + case release: + w.mouseButton &= ^button + } + + mouseDragged := w.mouseDragged + mouseDragStarted = w.mouseDragStarted + mouseOver := w.mouseOver + shouldMouseOut := w.objIsDragged(mouseOver) && !w.objIsDragged(coMouse) + mousePressed := w.mousePressed + + if action == release && mouseDragged != nil { + if mouseDragStarted { + mouseDragged.DragEnd() + w.mouseDragStarted = false + } + if shouldMouseOut { + w.mouseOut() + } + w.mouseDragged = nil + } + + _, tap := co.(fyne.Tappable) + secondary, altTap := co.(fyne.SecondaryTappable) + if tap || altTap { + switch action { + case press: + w.mousePressed = co + case release: + if co == mousePressed && button == desktop.MouseButtonSecondary && altTap { + secondary.TappedSecondary(ev) + } + } + } + + // Check for double click/tap on left mouse button + if action == release && button == desktop.MouseButtonPrimary && !mouseDragStarted { + w.mouseClickedHandleTapDoubleTap(co, ev) + } +} + +func (w *window) mouseClickedHandleMouseable(mev *desktop.MouseEvent, action action, wid desktop.Mouseable) { + switch action { + case press: + wid.MouseDown(mev) + case release: + mouseDragged := w.mouseDragged + mouseDraggedOffset := w.mouseDraggedOffset + if mouseDragged == nil { + wid.MouseUp(mev) + } else { + if dragged, ok := mouseDragged.(desktop.Mouseable); ok { + mev.Position = mev.AbsolutePosition.Subtract(mouseDraggedOffset) + dragged.MouseUp(mev) + } else { + wid.MouseUp(mev) + } + } + } +} + +func (w *window) mouseClickedHandleTapDoubleTap(co fyne.CanvasObject, ev *fyne.PointEvent) { + _, doubleTap := co.(fyne.DoubleTappable) + if doubleTap { + w.mouseClickCount++ + w.mouseLastClick = co + + mouseCancelFunc := w.mouseCancelFunc + if mouseCancelFunc != nil { + mouseCancelFunc() + return + } + + go w.waitForDoubleTap(co, ev) + } else { + if wid, ok := co.(fyne.Tappable); ok && co == w.mousePressed { + wid.Tapped(ev) + } + w.mousePressed = nil + } +} + +func (w *window) waitForDoubleTap(co fyne.CanvasObject, ev *fyne.PointEvent) { + ctx, mouseCancelFunc := context.WithDeadline(context.TODO(), time.Now().Add(w.driver.DoubleTapDelay())) + defer runOnMain(mouseCancelFunc) + runOnMain(func() { + w.mouseCancelFunc = mouseCancelFunc + }) + + <-ctx.Done() + + runOnMain(func() { + w.waitForDoubleTapEnded(co, ev) + }) +} + +func (w *window) waitForDoubleTapEnded(co fyne.CanvasObject, ev *fyne.PointEvent) { + if w.mouseClickCount == 2 && w.mouseLastClick == co { + if wid, ok := co.(fyne.DoubleTappable); ok { + wid.DoubleTapped(ev) + } + } else if co == w.mousePressed { + if wid, ok := co.(fyne.Tappable); ok { + wid.Tapped(ev) + } + } + + w.mouseClickCount = 0 + w.mousePressed = nil + w.mouseCancelFunc = nil + w.mouseLastClick = nil +} + +func (w *window) processMouseScrolled(xoff float64, yoff float64) { + mousePos := w.mousePos + co, pos, _ := w.findObjectAtPositionMatching(w.canvas, mousePos, func(object fyne.CanvasObject) bool { + _, ok := object.(fyne.Scrollable) + return ok + }) + switch wid := co.(type) { + case fyne.Scrollable: + if math.Abs(xoff) >= scrollAccelerateCutoff { + xoff *= scrollAccelerateRate + } + if math.Abs(yoff) >= scrollAccelerateCutoff { + yoff *= scrollAccelerateRate + } + + ev := &fyne.ScrollEvent{} + ev.Scrolled = fyne.NewDelta(float32(xoff)*scrollSpeed, float32(yoff)*scrollSpeed) + ev.Position = pos + ev.AbsolutePosition = mousePos + wid.Scrolled(ev) + } +} + +func (w *window) capturesTab(modifier fyne.KeyModifier) bool { + if ent, ok := w.canvas.Focused().(fyne.Tabbable); ok && ent.AcceptsTab() { + return true + } + + switch modifier { + case 0: + w.canvas.FocusNext() + case fyne.KeyModifierShift: + w.canvas.FocusPrevious() + } + + return false +} + +func (w *window) processKeyPressed(keyName fyne.KeyName, keyASCII fyne.KeyName, scancode int, action action, keyDesktopModifier fyne.KeyModifier) { + keyEvent := &fyne.KeyEvent{Name: keyName, Physical: fyne.HardwareKey{ScanCode: scancode}} + + pendingMenuToggle := w.menuTogglePending + w.menuTogglePending = desktop.KeyNone + pendingMenuDeactivation := w.menuDeactivationPending + w.menuDeactivationPending = desktop.KeyNone + switch action { + case release: + if action == release && keyName != "" { + switch keyName { + case pendingMenuToggle: + w.canvas.ToggleMenu() + case pendingMenuDeactivation: + if w.canvas.DismissMenu() { + return + } + } + } + + if w.canvas.Focused() != nil { + if focused, ok := w.canvas.Focused().(desktop.Keyable); ok { + focused.KeyUp(keyEvent) + } + } else if w.canvas.onKeyUp != nil { + w.canvas.onKeyUp(keyEvent) + } + return // ignore key up in other core events + case press: + switch keyName { + case desktop.KeyAltLeft, desktop.KeyAltRight: + // compensate for GLFW modifiers bug https://github.com/glfw/glfw/issues/1630 + if (runtime.GOOS == "linux" && keyDesktopModifier == 0) || (runtime.GOOS != "linux" && keyDesktopModifier == fyne.KeyModifierAlt) { + w.menuTogglePending = keyName + } + case fyne.KeyEscape: + w.menuDeactivationPending = keyName + } + if w.canvas.Focused() != nil { + if focused, ok := w.canvas.Focused().(desktop.Keyable); ok { + focused.KeyDown(keyEvent) + } + } else if w.canvas.onKeyDown != nil { + w.canvas.onKeyDown(keyEvent) + } + default: + // key repeat will fall through to TypedKey and TypedShortcut + } + + modifierOtherThanShift := (keyDesktopModifier & fyne.KeyModifierControl) | + (keyDesktopModifier & fyne.KeyModifierAlt) | + (keyDesktopModifier & fyne.KeyModifierSuper) + if (keyName == fyne.KeyTab && modifierOtherThanShift == 0 && !w.capturesTab(keyDesktopModifier)) || + w.triggersShortcut(keyName, keyASCII, keyDesktopModifier) { + return + } + + // No shortcut detected, pass down to TypedKey + focused := w.canvas.Focused() + if focused != nil { + focused.TypedKey(keyEvent) + } else if w.canvas.onTypedKey != nil { + w.canvas.onTypedKey(keyEvent) + } +} + +// charInput defines the character with modifiers callback which is called when a +// Unicode character is input. +// +// Characters do not map 1:1 to physical keys, as a key may produce zero, one or more characters. +func (w *window) processCharInput(char rune) { + if focused := w.canvas.Focused(); focused != nil { + focused.TypedRune(char) + } else if w.canvas.onTypedRune != nil { + w.canvas.onTypedRune(char) + } +} + +func (w *window) processFocused(focus bool) { + if focus { + if curWindow == nil { + if f := fyne.CurrentApp().Lifecycle().(*app.Lifecycle).OnEnteredForeground(); f != nil { + f() + } + } + curWindow = w + w.canvas.FocusGained() + } else { + w.canvas.FocusLost() + w.mousePos = fyne.Position{} + + // check whether another window was focused or not + if curWindow != w { + return + } + + curWindow = nil + if f := fyne.CurrentApp().Lifecycle().(*app.Lifecycle).OnExitedForeground(); f != nil { + f() + } + } +} + +func (w *window) triggersShortcut(localizedKeyName fyne.KeyName, key fyne.KeyName, modifier fyne.KeyModifier) bool { + ctrlMod := fyne.KeyModifierControl + if isMacOSRuntime() { + ctrlMod = fyne.KeyModifierSuper + } + // User pressing physical keys Ctrl+V while using a Russian (or any non-ASCII) keyboard layout + // is reported as a fyne.KeyUnknown key with Control modifier. We should still consider this + // as a "Paste" shortcut. + // See https://github.com/fyne-io/fyne/pull/2587 for discussion. + keyName := localizedKeyName + resemblesShortcut := (modifier&(fyne.KeyModifierControl|fyne.KeyModifierSuper) != 0) + if (localizedKeyName == fyne.KeyUnknown) && resemblesShortcut && key != fyne.KeyUnknown { + keyName = key + } + + var shortcut fyne.Shortcut + if modifier == ctrlMod { + switch keyName { + case fyne.KeyZ: // detect undo shortcut + shortcut = &fyne.ShortcutUndo{} + case fyne.KeyY: // detect redo shortcut + shortcut = &fyne.ShortcutRedo{} + case fyne.KeyV: // detect paste shortcut + shortcut = &fyne.ShortcutPaste{ + Clipboard: NewClipboard(), + } + case fyne.KeyC, fyne.KeyInsert: // detect copy shortcut + shortcut = &fyne.ShortcutCopy{ + Clipboard: NewClipboard(), + } + case fyne.KeyX: // detect cut shortcut + shortcut = &fyne.ShortcutCut{ + Clipboard: NewClipboard(), + } + case fyne.KeyA: // detect selectAll shortcut + shortcut = &fyne.ShortcutSelectAll{} + } + } + + if modifier == fyne.KeyModifierShift { + switch keyName { + case fyne.KeyInsert: // detect paste shortcut + shortcut = &fyne.ShortcutPaste{ + Clipboard: NewClipboard(), + } + case fyne.KeyDelete: // detect cut shortcut + shortcut = &fyne.ShortcutCut{ + Clipboard: NewClipboard(), + } + } + } + + if shortcut == nil && modifier != 0 && !isKeyModifier(keyName) && modifier != fyne.KeyModifierShift { + shortcut = &desktop.CustomShortcut{ + KeyName: keyName, + Modifier: modifier, + } + } + + if shortcut != nil { + if w.triggerMainMenuShortcut(shortcut) { + return true + } + if focused, ok := w.canvas.Focused().(fyne.Shortcutable); ok { + shouldRunShortcut := true + type selectableText interface { + fyne.Disableable + SelectedText() string + } + if selectableTextWid, ok := focused.(selectableText); ok && selectableTextWid.Disabled() { + shouldRunShortcut = shortcut.ShortcutName() == "Copy" + } + if shouldRunShortcut { + focused.TypedShortcut(shortcut) + } + return shouldRunShortcut + } + w.canvas.TypedShortcut(shortcut) + return true + } + + return false +} + +func (w *window) triggerMenuShortcut(sh fyne.Shortcut, m *fyne.Menu) bool { + for _, i := range m.Items { + if i.Shortcut != nil && i.Shortcut.ShortcutName() == sh.ShortcutName() { + if f := i.Action; f != nil { + f() + return true + } + } + + if i.ChildMenu != nil && w.triggerMenuShortcut(sh, i.ChildMenu) { + return true + } + } + + return false +} + +func (w *window) triggerMainMenuShortcut(sh fyne.Shortcut) bool { + if w.mainmenu == nil { + return false + } + + for _, m := range w.mainmenu.Items { + if w.triggerMenuShortcut(sh, m) { + return true + } + } + + return false +} + +func (w *window) RunWithContext(f func()) { + if w.isClosing() { + return + } + w.view().MakeContextCurrent() + + f() + + w.DetachCurrentContext() +} + +func (w *window) Context() any { + return nil +} + +func (w *window) runOnMainWhenCreated(fn func()) { + if w.view() != nil { + async.EnsureMain(fn) + return + } + + w.pending = append(w.pending, fn) +} + +func (d *gLDriver) CreateWindow(title string) (win fyne.Window) { + if runtime.GOOS != "js" { + async.EnsureMain(func() { + win = d.createWindow(title, true) + }) + return win + } + + // handling multiple windows by overlaying on the root for web + var root fyne.Window + hasVisible := false + for _, w := range d.windows { + if w.(*window).visible { + hasVisible = true + root = w + break + } + } + + if !hasVisible { + return d.createWindow(title, true) + } + + c := root.Canvas().(*glCanvas) + multi := c.webExtraWindows + if multi == nil { + multi = container.NewMultipleWindows() + multi.Resize(c.Size()) + c.webExtraWindows = multi + } + inner := container.NewInnerWindow(title, canvas.NewRectangle(color.Transparent)) + multi.Add(inner) + + return wrapInnerWindow(inner, root, d) +} + +func (d *gLDriver) createWindow(title string, decorate bool) fyne.Window { + var ret *window + if title == "" { + title = defaultTitle + } + + d.init() + + ret = &window{title: title, decorate: decorate, driver: d} + ret.canvas = newCanvas() + ret.canvas.context = ret + ret.SetIcon(ret.icon) + d.addWindow(ret) + return ret +} + +func (w *window) doShowAgain() { + if w.isClosing() { + return + } + + view := w.view() + if !build.IsWayland { + view.SetPos(w.xpos, w.ypos) + } + view.Show() + w.visible = true + + if w.fullScreen { + w.doSetFullScreen(true) + } + + w.RunWithContext(func() { + w.driver.repaintWindow(w) + }) +} + +func (w *window) isClosing() bool { + return w.closing || w.viewport == nil +} + +func (w *window) toggleVisible() { + if w.visible { + w.Hide() + } else { + w.Show() + } +} + +func (d *gLDriver) CreateSplashWindow() fyne.Window { + win := d.createWindow("", false) + win.SetPadded(false) + win.CenterOnScreen() + return win +} + +func (d *gLDriver) AllWindows() []fyne.Window { + return d.windows +} + +func isKeyModifier(keyName fyne.KeyName) bool { + return keyName == desktop.KeyShiftLeft || keyName == desktop.KeyShiftRight || + keyName == desktop.KeyControlLeft || keyName == desktop.KeyControlRight || + keyName == desktop.KeyAltLeft || keyName == desktop.KeyAltRight || + keyName == desktop.KeySuperLeft || keyName == desktop.KeySuperRight +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/window_darwin.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/window_darwin.go new file mode 100644 index 0000000..8a8ea4b --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/window_darwin.go @@ -0,0 +1,39 @@ +//go:build darwin + +package glfw + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Foundation -framework AppKit + +#import + +void setFullScreen(bool full, void *window); +*/ +import "C" + +import ( + "runtime" + + "fyne.io/fyne/v2/driver" +) + +// assert we are implementing driver.NativeWindow +var _ driver.NativeWindow = (*window)(nil) + +func (w *window) RunNative(f func(any)) { + context := driver.MacWindowContext{} + if v := w.view(); v != nil { + context.NSWindow = uintptr(v.GetCocoaWindow()) + } + + f(context) +} + +func (w *window) doSetFullScreen(full bool) { + if runtime.GOOS == "darwin" { + win := w.view().GetCocoaWindow() + C.setFullScreen(C.bool(full), win) + return + } +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/window_darwin.m b/vendor/fyne.io/fyne/v2/internal/driver/glfw/window_darwin.m new file mode 100644 index 0000000..7224f81 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/window_darwin.m @@ -0,0 +1,14 @@ +#import +#import + +void setFullScreen(bool full, void *win) { + NSWindow *window = (NSWindow*)win; + + NSUInteger masks = [window styleMask]; + bool isFull = masks & NSWindowStyleMaskFullScreen; + if (isFull == full) { + return; + } + + [window toggleFullScreen:NULL]; +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/window_desktop.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/window_desktop.go new file mode 100644 index 0000000..8bcd31a --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/window_desktop.go @@ -0,0 +1,819 @@ +//go:build !wasm && !test_web_driver + +package glfw + +import ( + "bytes" + "context" + "image" + _ "image/png" // for the icon + "os" + "runtime" + "strings" + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/driver/desktop" + "fyne.io/fyne/v2/internal/async" + "fyne.io/fyne/v2/internal/build" + "fyne.io/fyne/v2/internal/cache" + "fyne.io/fyne/v2/internal/painter" + "fyne.io/fyne/v2/internal/painter/gl" + "fyne.io/fyne/v2/internal/scale" + "fyne.io/fyne/v2/internal/svg" + "fyne.io/fyne/v2/storage" + + "github.com/go-gl/glfw/v3.3/glfw" +) + +const ( + defaultTitle = "Fyne Application" + disableDPIDetectionEnvKey = "FYNE_DISABLE_DPI_DETECTION" +) + +// Input modes. +const ( + CursorMode glfw.InputMode = glfw.CursorMode + StickyKeysMode glfw.InputMode = glfw.StickyKeysMode + StickyMouseButtonsMode glfw.InputMode = glfw.StickyMouseButtonsMode + LockKeyMods glfw.InputMode = glfw.LockKeyMods + RawMouseMotion glfw.InputMode = glfw.RawMouseMotion +) + +// Cursor mode values. +const ( + CursorNormal int = glfw.CursorNormal + CursorHidden int = glfw.CursorHidden + CursorDisabled int = glfw.CursorDisabled +) + +var cursors [desktop.HiddenCursor + 1]*glfw.Cursor + +func initCursors() { + cursors = [desktop.HiddenCursor + 1]*glfw.Cursor{ + desktop.DefaultCursor: glfw.CreateStandardCursor(glfw.ArrowCursor), + desktop.TextCursor: glfw.CreateStandardCursor(glfw.IBeamCursor), + desktop.CrosshairCursor: glfw.CreateStandardCursor(glfw.CrosshairCursor), + desktop.PointerCursor: glfw.CreateStandardCursor(glfw.HandCursor), + desktop.HResizeCursor: glfw.CreateStandardCursor(glfw.HResizeCursor), + desktop.VResizeCursor: glfw.CreateStandardCursor(glfw.VResizeCursor), + desktop.HiddenCursor: nil, + } +} + +// Declare conformity to Window interface +var _ fyne.Window = (*window)(nil) + +type window struct { + viewport *glfw.Window + created bool + decorate bool + closing bool + fixedSize bool + + cursor desktop.Cursor + customCursor *glfw.Cursor + canvas *glCanvas + driver *gLDriver + title string + icon fyne.Resource + mainmenu *fyne.MainMenu + + master bool + fullScreen bool + centered bool + visible bool + + mousePos fyne.Position + mouseDragged fyne.Draggable + mouseDraggedObjStart fyne.Position + mouseDraggedOffset fyne.Position + mouseDragPos fyne.Position + mouseDragStarted bool + mouseButton desktop.MouseButton + mouseOver desktop.Hoverable + mouseLastClick fyne.CanvasObject + mousePressed fyne.CanvasObject + mouseClickCount int + mouseCancelFunc context.CancelFunc + + onClosed func() + onCloseIntercepted func() + + menuTogglePending fyne.KeyName + menuDeactivationPending fyne.KeyName + + xpos, ypos int + width, height int + requestedWidth, requestedHeight int + shouldWidth, shouldHeight int + shouldExpand bool + + pending []func() + + lastWalkedTime time.Time +} + +func (w *window) SetFullScreen(full bool) { + w.fullScreen = full + + if w.view() != nil { + async.EnsureMain(func() { + w.doSetFullScreen(full) + }) + } +} + +func (w *window) CenterOnScreen() { + if build.IsWayland { + return + } + + w.centered = true + + w.runOnMainWhenCreated(w.doCenterOnScreen) +} + +func (w *window) SetOnDropped(dropped func(pos fyne.Position, items []fyne.URI)) { + w.runOnMainWhenCreated(func() { + w.viewport.SetDropCallback(func(win *glfw.Window, names []string) { + if dropped == nil { + return + } + + uris := make([]fyne.URI, len(names)) + for i, name := range names { + uris[i] = storage.NewFileURI(name) + } + + dropped(w.mousePos, uris) + }) + }) +} + +func (w *window) doCenterOnScreen() { + viewWidth, viewHeight := w.screenSize(w.canvas.size) + if w.width > viewWidth { // in case our window has not called back to canvas size yet + viewWidth = w.width + } + if w.height > viewHeight { + viewHeight = w.height + } + + // get window dimensions in pixels + monitor := w.getMonitorForWindow() + monMode := monitor.GetVideoMode() + + // these come into play when dealing with multiple monitors + monX, monY := monitor.GetPos() + + // math them to the middle + newX := (monMode.Width-viewWidth)/2 + monX + newY := (monMode.Height-viewHeight)/2 + monY + + // set new window coordinates + w.viewport.SetPos(newX, newY) +} + +func (w *window) RequestFocus() { + if build.IsWayland || w.view() == nil { + return + } + + w.runOnMainWhenCreated(w.viewport.Focus) +} + +func (w *window) SetIcon(icon fyne.Resource) { + w.icon = icon + if build.IsWayland { + return + } + + if icon == nil { + appIcon := fyne.CurrentApp().Icon() + if appIcon != nil { + w.SetIcon(appIcon) + } + return + } + + w.runOnMainWhenCreated(func() { + if w.icon == nil { + w.viewport.SetIcon(nil) + return + } + + var img image.Image + if svg.IsResourceSVG(w.icon) { + img = painter.PaintImage(&canvas.Image{Resource: w.icon}, nil, windowIconSize, windowIconSize) + } else { + pix, _, err := image.Decode(bytes.NewReader(w.icon.Content())) + if err != nil { + fyne.LogError("Failed to decode image for window icon", err) + return + } + img = pix + } + + w.viewport.SetIcon([]image.Image{img}) + }) +} + +func (w *window) SetMaster() { + w.master = true +} + +func (w *window) fitContent() { + if w.canvas.Content() == nil || (w.fullScreen && w.visible) { + return + } + + if w.isClosing() { + return + } + + minWidth, minHeight := w.minSizeOnScreen() + view := w.viewport + w.shouldWidth, w.shouldHeight = w.width, w.height + if w.width < minWidth || w.height < minHeight { + if w.width < minWidth { + w.shouldWidth = minWidth + } + if w.height < minHeight { + w.shouldHeight = minHeight + } + w.shouldExpand = true // queue the resize to happen on main + } + if w.fixedSize { + if w.shouldWidth > w.requestedWidth { + w.requestedWidth = w.shouldWidth + } + if w.shouldHeight > w.requestedHeight { + w.requestedHeight = w.shouldHeight + } + view.SetSizeLimits(w.requestedWidth, w.requestedHeight, w.requestedWidth, w.requestedHeight) + } else { + view.SetSizeLimits(minWidth, minHeight, glfw.DontCare, glfw.DontCare) + } +} + +// getMonitorScale returns the scale factor for a given monitor, handling platform-specific cases +func getMonitorScale(monitor *glfw.Monitor) float32 { + widthMm, heightMm := monitor.GetPhysicalSize() + if runtime.GOOS == "linux" && widthMm == 60 && heightMm == 60 { // Steam Deck incorrectly reports 6cm square! + return 1.0 + } + widthPx := monitor.GetVideoMode().Width + return calculateDetectedScale(widthMm, widthPx) +} + +// getScaledMonitorSize returns the monitor dimensions adjusted for scaling +func getScaledMonitorSize(monitor *glfw.Monitor) fyne.Size { + videoMode := monitor.GetVideoMode() + scale := getMonitorScale(monitor) + + scaledWidth := float32(videoMode.Width) / scale + scaledHeight := float32(videoMode.Height) / scale + return fyne.NewSize(scaledWidth, scaledHeight) +} + +func (w *window) getMonitorForWindow() *glfw.Monitor { + if !build.IsWayland { + x, y := w.xpos, w.ypos + if w.fullScreen { + x, y = w.viewport.GetPos() + } + xOff := x + (w.width / 2) + yOff := y + (w.height / 2) + + for _, monitor := range glfw.GetMonitors() { + x, y := monitor.GetPos() + + if x > xOff || y > yOff { + continue + } + + scaledSize := getScaledMonitorSize(monitor) + if x+int(scaledSize.Width) <= xOff || y+int(scaledSize.Height) <= yOff { + continue + } + + return monitor + } + } + + // try built-in function to detect monitor if above logic didn't succeed + // if it doesn't work then return primary monitor as default + monitor := w.viewport.GetMonitor() + if monitor == nil { + monitor = glfw.GetPrimaryMonitor() + } + return monitor +} + +func (w *window) detectScale() float32 { + if build.IsWayland { // Wayland controls scale through content scaling + return 1 + } + + // check if DPI detection is disabled + env := os.Getenv(disableDPIDetectionEnvKey) + if strings.EqualFold(env, "true") || strings.EqualFold(env, "t") || env == "1" { + return 1 + } + + monitor := w.getMonitorForWindow() + if monitor == nil { + return 1 + } + + return getMonitorScale(monitor) +} + +func (w *window) moved(_ *glfw.Window, x, y int) { + w.processMoved(x, y) +} + +func (w *window) resized(_ *glfw.Window, width, height int) { + w.processResized(width, height) +} + +func (w *window) scaled(_ *glfw.Window, x float32, y float32) { + if !build.IsWayland { // other platforms handle this using older APIs + return + } + + w.canvas.texScale = x + w.canvas.Refresh(w.canvas.content) +} + +func (w *window) frameSized(_ *glfw.Window, width, height int) { + w.processFrameSized(width, height) +} + +func (w *window) refresh(_ *glfw.Window) { + w.processRefresh() +} + +func (w *window) closed(viewport *glfw.Window) { + if viewport != nil { + viewport.SetShouldClose(false) // reset the closed flag until we check the veto in processClosed + } + + w.processClosed() +} + +func fyneToNativeCursor(cursor desktop.Cursor) (*glfw.Cursor, bool) { + cursorType, standard := cursor.(desktop.StandardCursor) + if !standard { + img, x, y := cursor.Image() + if img == nil { + return nil, true + } + return glfw.CreateCursor(img, x, y), true + } + + if cursorType < 0 || cursorType >= desktop.StandardCursor(len(cursors)) { + return cursors[desktop.DefaultCursor], false + } + + return cursors[cursorType], false +} + +func (w *window) SetCursor(cursor *glfw.Cursor) { + async.EnsureMain(func() { + w.viewport.SetCursor(cursor) + }) +} + +func (w *window) setCustomCursor(rawCursor *glfw.Cursor, isCustomCursor bool) { + if w.customCursor != nil { + w.customCursor.Destroy() + w.customCursor = nil + } + if isCustomCursor { + w.customCursor = rawCursor + } +} + +func (w *window) mouseMoved(_ *glfw.Window, xpos, ypos float64) { + w.processMouseMoved(xpos, ypos) +} + +func (w *window) mouseClicked(_ *glfw.Window, btn glfw.MouseButton, action glfw.Action, mods glfw.ModifierKey) { + button, modifiers := convertMouseButton(btn, mods) + mouseAction := convertAction(action) + + w.processMouseClicked(button, mouseAction, modifiers) +} + +func (w *window) mouseScrolled(viewport *glfw.Window, xoff float64, yoff float64) { + if runtime.GOOS != "darwin" && xoff == 0 && + (viewport.GetKey(glfw.KeyLeftShift) == glfw.Press || + viewport.GetKey(glfw.KeyRightShift) == glfw.Press) { + xoff, yoff = yoff, xoff + } + + w.processMouseScrolled(xoff, yoff) +} + +func convertMouseButton(btn glfw.MouseButton, mods glfw.ModifierKey) (desktop.MouseButton, fyne.KeyModifier) { + modifier := desktopModifier(mods) + rightClick := false + if runtime.GOOS == "darwin" { + if modifier&fyne.KeyModifierControl != 0 { + rightClick = true + modifier &^= fyne.KeyModifierControl + } + if modifier&fyne.KeyModifierSuper != 0 { + modifier |= fyne.KeyModifierControl + modifier &^= fyne.KeyModifierSuper + } + } + + switch btn { + case glfw.MouseButton1: + if rightClick { + return desktop.MouseButtonSecondary, modifier + } + return desktop.MouseButtonPrimary, modifier + case glfw.MouseButton2: + return desktop.MouseButtonSecondary, modifier + case glfw.MouseButton3: + return desktop.MouseButtonTertiary, modifier + default: + return 0, modifier + } +} + +//gocyclo:ignore +func glfwKeyToKeyName(key glfw.Key) fyne.KeyName { + switch key { + // numbers - lookup by code to avoid AZERTY using the symbol name instead of number + case glfw.Key0, glfw.KeyKP0: + return fyne.Key0 + case glfw.Key1, glfw.KeyKP1: + return fyne.Key1 + case glfw.Key2, glfw.KeyKP2: + return fyne.Key2 + case glfw.Key3, glfw.KeyKP3: + return fyne.Key3 + case glfw.Key4, glfw.KeyKP4: + return fyne.Key4 + case glfw.Key5, glfw.KeyKP5: + return fyne.Key5 + case glfw.Key6, glfw.KeyKP6: + return fyne.Key6 + case glfw.Key7, glfw.KeyKP7: + return fyne.Key7 + case glfw.Key8, glfw.KeyKP8: + return fyne.Key8 + case glfw.Key9, glfw.KeyKP9: + return fyne.Key9 + + // non-printable + case glfw.KeyEscape: + return fyne.KeyEscape + case glfw.KeyEnter: + return fyne.KeyReturn + case glfw.KeyTab: + return fyne.KeyTab + case glfw.KeyBackspace: + return fyne.KeyBackspace + case glfw.KeyInsert: + return fyne.KeyInsert + case glfw.KeyDelete: + return fyne.KeyDelete + case glfw.KeyRight: + return fyne.KeyRight + case glfw.KeyLeft: + return fyne.KeyLeft + case glfw.KeyDown: + return fyne.KeyDown + case glfw.KeyUp: + return fyne.KeyUp + case glfw.KeyPageUp: + return fyne.KeyPageUp + case glfw.KeyPageDown: + return fyne.KeyPageDown + case glfw.KeyHome: + return fyne.KeyHome + case glfw.KeyEnd: + return fyne.KeyEnd + + case glfw.KeySpace: + return fyne.KeySpace + case glfw.KeyKPEnter: + return fyne.KeyEnter + + // desktop + case glfw.KeyLeftShift: + return desktop.KeyShiftLeft + case glfw.KeyRightShift: + return desktop.KeyShiftRight + case glfw.KeyLeftControl: + return desktop.KeyControlLeft + case glfw.KeyRightControl: + return desktop.KeyControlRight + case glfw.KeyLeftAlt: + return desktop.KeyAltLeft + case glfw.KeyRightAlt: + return desktop.KeyAltRight + case glfw.KeyLeftSuper: + return desktop.KeySuperLeft + case glfw.KeyRightSuper: + return desktop.KeySuperRight + case glfw.KeyMenu: + return desktop.KeyMenu + case glfw.KeyPrintScreen: + return desktop.KeyPrintScreen + case glfw.KeyCapsLock: + return desktop.KeyCapsLock + + // functions + case glfw.KeyF1: + return fyne.KeyF1 + case glfw.KeyF2: + return fyne.KeyF2 + case glfw.KeyF3: + return fyne.KeyF3 + case glfw.KeyF4: + return fyne.KeyF4 + case glfw.KeyF5: + return fyne.KeyF5 + case glfw.KeyF6: + return fyne.KeyF6 + case glfw.KeyF7: + return fyne.KeyF7 + case glfw.KeyF8: + return fyne.KeyF8 + case glfw.KeyF9: + return fyne.KeyF9 + case glfw.KeyF10: + return fyne.KeyF10 + case glfw.KeyF11: + return fyne.KeyF11 + case glfw.KeyF12: + return fyne.KeyF12 + } + + return fyne.KeyUnknown +} + +func keyCodeToKeyName(code string) fyne.KeyName { + if len(code) != 1 { + return fyne.KeyUnknown + } + + char := code[0] + if char >= 'a' && char <= 'z' { + return fyne.KeyName(char ^ ('a' - 'A')) // Corresponding KeyName is uppercase. Convert with simple bit flip. + } + + switch char { + case '[': + return fyne.KeyLeftBracket + case '\\': + return fyne.KeyBackslash + case ']': + return fyne.KeyRightBracket + case '\'': + return fyne.KeyApostrophe + case ',': + return fyne.KeyComma + case '-': + return fyne.KeyMinus + case '.': + return fyne.KeyPeriod + case '/': + return fyne.KeySlash + case '*': + return fyne.KeyAsterisk + case '`': + return fyne.KeyBackTick + case ';': + return fyne.KeySemicolon + case '+': + return fyne.KeyPlus + case '=': + return fyne.KeyEqual + } + + return fyne.KeyUnknown +} + +func keyToName(code glfw.Key, scancode int) fyne.KeyName { + ret := glfwKeyToKeyName(code) + if ret != fyne.KeyUnknown { + return ret + } + + keyName := glfw.GetKeyName(code, scancode) + return keyCodeToKeyName(keyName) +} + +func convertAction(action glfw.Action) action { + switch action { + case glfw.Press: + return press + case glfw.Release: + return release + case glfw.Repeat: + return repeat + } + panic("Could not convert glfw.Action.") +} + +func convertASCII(key glfw.Key) fyne.KeyName { + if key < glfw.KeyA || key > glfw.KeyZ { + return fyne.KeyUnknown + } + + return fyne.KeyName(rune(key)) +} + +func (w *window) keyPressed(_ *glfw.Window, key glfw.Key, scancode int, action glfw.Action, mods glfw.ModifierKey) { + keyName := keyToName(key, scancode) + keyDesktopModifier := desktopModifier(mods) + w.driver.currentKeyModifiers = desktopModifierCorrected(mods, key, action) + keyAction := convertAction(action) + keyASCII := convertASCII(key) + + w.processKeyPressed(keyName, keyASCII, scancode, keyAction, keyDesktopModifier) +} + +func desktopModifier(mods glfw.ModifierKey) fyne.KeyModifier { + var m fyne.KeyModifier + if (mods & glfw.ModShift) != 0 { + m |= fyne.KeyModifierShift + } + if (mods & glfw.ModControl) != 0 { + m |= fyne.KeyModifierControl + } + if (mods & glfw.ModAlt) != 0 { + m |= fyne.KeyModifierAlt + } + if (mods & glfw.ModSuper) != 0 { + m |= fyne.KeyModifierSuper + } + return m +} + +func desktopModifierCorrected(mods glfw.ModifierKey, key glfw.Key, action glfw.Action) fyne.KeyModifier { + // On X11, pressing/releasing modifier keys does not include newly pressed/released keys in 'mod' mask. + // https://github.com/glfw/glfw/issues/1630 + if action == glfw.Press { + mods |= glfwKeyToModifier(key) + } else { + mods &= ^glfwKeyToModifier(key) + } + return desktopModifier(mods) +} + +func glfwKeyToModifier(key glfw.Key) glfw.ModifierKey { + switch key { + case glfw.KeyLeftControl, glfw.KeyRightControl: + return glfw.ModControl + case glfw.KeyLeftAlt, glfw.KeyRightAlt: + return glfw.ModAlt + case glfw.KeyLeftShift, glfw.KeyRightShift: + return glfw.ModShift + case glfw.KeyLeftSuper, glfw.KeyRightSuper: + return glfw.ModSuper + default: + return 0 + } +} + +// charInput defines the character with modifiers callback which is called when a +// Unicode character is input. +// +// Characters do not map 1:1 to physical keys, as a key may produce zero, one or more characters. +func (w *window) charInput(viewport *glfw.Window, char rune) { + w.processCharInput(char) +} + +func (w *window) focused(_ *glfw.Window, focused bool) { + w.processFocused(focused) +} + +func (w *window) DetachCurrentContext() { + glfw.DetachCurrentContext() +} + +func (w *window) RescaleContext() { + if w.isClosing() { + return + } + w.fitContent() + + if w.fullScreen { + w.width, w.height = w.viewport.GetSize() + scaledFull := fyne.NewSize( + scale.ToFyneCoordinate(w.canvas, w.width), + scale.ToFyneCoordinate(w.canvas, w.height)) + w.canvas.Resize(scaledFull) + return + } + + size := w.canvas.size.Max(w.canvas.MinSize()) + newWidth, newHeight := w.screenSize(size) + w.viewport.SetSize(newWidth, newHeight) + + // Ensure textures re-rasterize at the new scale + cache.DeleteTextTexturesFor(w.canvas) + w.canvas.content.Refresh() +} + +func (w *window) create() { + if !build.IsWayland { + // make the window hidden, we will set it up and then show it later + glfw.WindowHint(glfw.Visible, glfw.False) + } + if w.decorate { + glfw.WindowHint(glfw.Decorated, glfw.True) + } else { + glfw.WindowHint(glfw.Decorated, glfw.False) + } + if w.fixedSize { + glfw.WindowHint(glfw.Resizable, glfw.False) + } else { + glfw.WindowHint(glfw.Resizable, glfw.True) + } + glfw.WindowHint(glfw.AutoIconify, glfw.False) + initWindowHints() + + pixWidth, pixHeight := w.screenSize(w.canvas.size) + pixWidth = int(fyne.Max(float32(pixWidth), float32(w.width))) + if pixWidth == 0 { + pixWidth = 10 + } + pixHeight = int(fyne.Max(float32(pixHeight), float32(w.height))) + if pixHeight == 0 { + pixHeight = 10 + } + + win, err := glfw.CreateWindow(pixWidth, pixHeight, w.title, nil, nil) + if err != nil { + w.driver.initFailed("window creation error", err) + return + } + + w.viewport = win + if w.view() == nil { // something went wrong above, it will have been logged + return + } + + // run the GL init on the draw thread + w.RunWithContext(func() { + w.canvas.SetPainter(gl.NewPainter(w.canvas, w)) + w.canvas.Painter().Init() + }) + + w.setDarkMode() + + win.SetCloseCallback(w.closed) + win.SetPosCallback(w.moved) + win.SetSizeCallback(w.resized) + win.SetFramebufferSizeCallback(w.frameSized) + win.SetRefreshCallback(w.refresh) + win.SetContentScaleCallback(w.scaled) + win.SetCursorPosCallback(w.mouseMoved) + win.SetMouseButtonCallback(w.mouseClicked) + win.SetScrollCallback(w.mouseScrolled) + win.SetKeyCallback(w.keyPressed) + win.SetCharCallback(w.charInput) + win.SetFocusCallback(w.focused) + + w.canvas.detectedScale = w.detectScale() + w.canvas.scale = w.calculatedScale() + w.canvas.texScale = w.detectTextureScale() + // update window size now we have scaled detected + w.fitContent() + + w.drainPendingEvents() + + if w.FixedSize() && (w.requestedWidth == 0 || w.requestedHeight == 0) { + bigEnough := w.canvas.canvasSize(w.canvas.Content().MinSize()) + w.width, w.height = scale.ToScreenCoordinate(w.canvas, bigEnough.Width), scale.ToScreenCoordinate(w.canvas, bigEnough.Height) + w.shouldWidth, w.shouldHeight = w.width, w.height + } + + w.requestedWidth, w.requestedHeight = w.width, w.height + // order of operation matters so we do these last items in order + w.viewport.SetSize(w.shouldWidth, w.shouldHeight) // ensure we requested latest size +} + +func (w *window) view() *glfw.Window { + if w.closing { + return nil + } + return w.viewport +} + +// wrapInnerWindow is a no-op to match what the web driver provides +func wrapInnerWindow(*container.InnerWindow, fyne.Window, *gLDriver) fyne.Window { + return nil +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/window_notdarwin.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/window_notdarwin.go new file mode 100644 index 0000000..263d892 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/window_notdarwin.go @@ -0,0 +1,22 @@ +//go:build !darwin + +package glfw + +import "time" + +const desktopDefaultDoubleTapDelay = 300 * time.Millisecond + +func (w *window) doSetFullScreen(full bool) { + monitor := w.getMonitorForWindow() + mode := monitor.GetVideoMode() + + if full { + w.viewport.SetMonitor(monitor, 0, 0, mode.Width, mode.Height, mode.RefreshRate) + } else { + if w.width == 0 && w.height == 0 { // if we were fullscreen on creation... + s := w.canvas.Size().Max(w.canvas.MinSize()) + w.width, w.height = w.screenSize(s) + } + w.viewport.SetMonitor(nil, w.xpos, w.ypos, w.width, w.height, 0) + } +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/window_notwindows.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/window_notwindows.go new file mode 100644 index 0000000..0310843 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/window_notwindows.go @@ -0,0 +1,15 @@ +//go:build !windows + +package glfw + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/scale" +) + +func (w *window) setDarkMode() { +} + +func (w *window) computeCanvasSize(width, height int) fyne.Size { + return fyne.NewSize(scale.ToFyneCoordinate(w.canvas, width), scale.ToFyneCoordinate(w.canvas, height)) +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/window_notxdg.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/window_notxdg.go new file mode 100644 index 0000000..e5c5226 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/window_notxdg.go @@ -0,0 +1,16 @@ +//go:build !linux && !freebsd && !openbsd && !netbsd + +package glfw + +import "fyne.io/fyne/v2" + +func (w *window) platformResize(canvasSize fyne.Size) { + d, ok := fyne.CurrentApp().Driver().(*gLDriver) + if !ok { // don't wait to redraw in this way if we are running on test + w.canvas.Resize(canvasSize) + return + } + + w.canvas.Resize(canvasSize) + d.repaintWindow(w) +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/window_wasm.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/window_wasm.go new file mode 100644 index 0000000..2b1b2b7 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/window_wasm.go @@ -0,0 +1,706 @@ +//go:build wasm || test_web_driver + +package glfw + +import ( + "context" + _ "image/png" // for the icon + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/driver/desktop" + "fyne.io/fyne/v2/internal/cache" + "fyne.io/fyne/v2/internal/painter/gl" + "fyne.io/fyne/v2/internal/scale" + + "github.com/fyne-io/glfw-js" +) + +type Cursor struct { + JSName string +} + +const defaultTitle = "Fyne Application" + +// Input modes. +const ( + CursorMode glfw.InputMode = glfw.CursorMode + StickyKeysMode glfw.InputMode = glfw.StickyKeysMode + StickyMouseButtonsMode glfw.InputMode = glfw.StickyMouseButtonsMode + LockKeyMods glfw.InputMode = glfw.LockKeyMods + RawMouseMotion glfw.InputMode = glfw.RawMouseMotion +) + +// Cursor mode values. +const ( + CursorNormal int = glfw.CursorNormal + CursorHidden int = glfw.CursorHidden + CursorDisabled int = glfw.CursorDisabled +) + +// Declare conformity to Window interface +var _ fyne.Window = (*window)(nil) + +type window struct { + viewport *glfw.Window + created bool + decorate bool + closing bool + fixedSize bool + + cursor desktop.Cursor + canvas *glCanvas + driver *gLDriver + title string + icon fyne.Resource + mainmenu *fyne.MainMenu + + master bool + fullScreen bool + centered bool + visible bool + + mousePos fyne.Position + mouseDragged fyne.Draggable + mouseDraggedObjStart fyne.Position + mouseDraggedOffset fyne.Position + mouseDragPos fyne.Position + mouseDragStarted bool + mouseButton desktop.MouseButton + mouseOver desktop.Hoverable + mouseLastClick fyne.CanvasObject + mousePressed fyne.CanvasObject + mouseClickCount int + mouseCancelFunc context.CancelFunc + + onClosed func() + onCloseIntercepted func() + + menuTogglePending fyne.KeyName + menuDeactivationPending fyne.KeyName + + xpos, ypos int + width, height int + requestedWidth, requestedHeight int + shouldWidth, shouldHeight int + shouldExpand bool + + pending []func() + + lastWalkedTime time.Time +} + +func (w *window) SetFullScreen(full bool) { + w.fullScreen = true +} + +// centerOnScreen handles the logic for centering a window +func (w *window) CenterOnScreen() { + // FIXME: not supported with WebGL + w.centered = true +} + +func (w *window) SetOnDropped(dropped func(pos fyne.Position, items []fyne.URI)) { + // FIXME: not implemented yet +} + +func (w *window) doCenterOnScreen() { + // FIXME: no meaning for defining center on screen in WebGL +} + +func (w *window) RequestFocus() { + // FIXME: no meaning for defining focus in WebGL +} + +func (w *window) SetIcon(icon fyne.Resource) { + // FIXME: no support for SetIcon yet +} + +func (w *window) SetMaster() { + // FIXME: there could really only be one window +} + +func (w *window) fitContent() { + w.shouldWidth, w.shouldHeight = w.requestedWidth, w.requestedHeight +} + +func (w *window) getMonitorForWindow() *glfw.Monitor { + return glfw.GetPrimaryMonitor() +} + +func scaleForDpi(xdpi int) float32 { + switch { + case xdpi > 1000: + // assume that this is a mistake and bail + return float32(1.0) + case xdpi > 192: + return float32(1.5) + case xdpi > 144: + return float32(1.35) + case xdpi > 120: + return float32(1.2) + default: + return float32(1.0) + } +} + +func (w *window) detectScale() float32 { + return scaleForDpi(int(96)) +} + +func (w *window) moved(_ *glfw.Window, x, y int) { + runOnMain(func() { + w.processMoved(x, y) + }) +} + +func (w *window) resized(_ *glfw.Window, width, height int) { + runOnMain(func() { + w.canvas.scale = w.calculatedScale() + w.processResized(width, height) + }) +} + +func (w *window) frameSized(_ *glfw.Window, width, height int) { + runOnMain(func() { + w.processFrameSized(width, height) + }) +} + +func (w *window) refresh(_ *glfw.Window) { + runOnMain(w.processRefresh) +} + +func (w *window) closed(viewport *glfw.Window) { + runOnMain(func() { + viewport.SetShouldClose(false) // reset the closed flag until we check the veto in processClosed + + w.processClosed() + }) +} + +func fyneToNativeCursor(cursor desktop.Cursor) (*Cursor, bool) { + if _, ok := cursor.(desktop.StandardCursor); !ok { + return nil, false // Custom cursors not implemented yet. + } + + name := "default" + switch cursor { + case desktop.TextCursor: + name = "text" + case desktop.CrosshairCursor: + name = "crosshair" + case desktop.DefaultCursor: + name = "default" + case desktop.PointerCursor: + name = "pointer" + case desktop.HResizeCursor: + name = "ew-resize" + case desktop.VResizeCursor: + name = "ns-resize" + case desktop.HiddenCursor: + name = "none" + } + + return &Cursor{JSName: name}, false +} + +func (w *window) SetCursor(cursor *Cursor) { + setCursor(cursor.JSName) +} + +func (w *window) setCustomCursor(rawCursor *Cursor, isCustomCursor bool) { +} + +func (w *window) mouseMoved(_ *glfw.Window, xpos, ypos float64) { + runOnMain(func() { + w.processMouseMoved(w.scaleInput(xpos), w.scaleInput(ypos)) + }) +} + +func (w *window) mouseClicked(viewport *glfw.Window, btn glfw.MouseButton, action glfw.Action, mods glfw.ModifierKey) { + runOnMain(func() { + button, modifiers := convertMouseButton(btn, mods) + mouseAction := convertAction(action) + + w.processMouseClicked(button, mouseAction, modifiers) + }) +} + +func (w *window) mouseScrolled(viewport *glfw.Window, xoff, yoff float64) { + runOnMain(func() { + if xoff == 0 && + (viewport.GetKey(glfw.KeyLeftShift) == glfw.Press || + viewport.GetKey(glfw.KeyRightShift) == glfw.Press) { + xoff, yoff = yoff, xoff + } + + w.processMouseScrolled(xoff, yoff) + }) +} + +func convertMouseButton(btn glfw.MouseButton, mods glfw.ModifierKey) (desktop.MouseButton, fyne.KeyModifier) { + modifier := desktopModifier(mods) + rightClick := false + if isMacOSRuntime() { + if modifier&fyne.KeyModifierControl != 0 { + rightClick = true + modifier &^= fyne.KeyModifierControl + } + if modifier&fyne.KeyModifierSuper != 0 { + modifier |= fyne.KeyModifierControl + modifier &^= fyne.KeyModifierSuper + } + } + + switch btn { + case glfw.MouseButton1: + if rightClick { + return desktop.MouseButtonSecondary, modifier + } + return desktop.MouseButtonPrimary, modifier + case glfw.MouseButton2: + return desktop.MouseButtonSecondary, modifier + case glfw.MouseButton3: + return desktop.MouseButtonTertiary, modifier + default: + return 0, modifier + } +} + +//gocyclo:ignore +func glfwKeyToKeyName(key glfw.Key) fyne.KeyName { + switch key { + // numbers - lookup by code to avoid AZERTY using the symbol name instead of number + case glfw.Key0, glfw.KeyKP0: + return fyne.Key0 + case glfw.Key1, glfw.KeyKP1: + return fyne.Key1 + case glfw.Key2, glfw.KeyKP2: + return fyne.Key2 + case glfw.Key3, glfw.KeyKP3: + return fyne.Key3 + case glfw.Key4, glfw.KeyKP4: + return fyne.Key4 + case glfw.Key5, glfw.KeyKP5: + return fyne.Key5 + case glfw.Key6, glfw.KeyKP6: + return fyne.Key6 + case glfw.Key7, glfw.KeyKP7: + return fyne.Key7 + case glfw.Key8, glfw.KeyKP8: + return fyne.Key8 + case glfw.Key9, glfw.KeyKP9: + return fyne.Key9 + + // non-printable + case glfw.KeyEscape: + return fyne.KeyEscape + case glfw.KeyEnter: + return fyne.KeyReturn + case glfw.KeyTab: + return fyne.KeyTab + case glfw.KeyBackspace: + return fyne.KeyBackspace + case glfw.KeyInsert: + return fyne.KeyInsert + case glfw.KeyDelete: + return fyne.KeyDelete + case glfw.KeyRight: + return fyne.KeyRight + case glfw.KeyLeft: + return fyne.KeyLeft + case glfw.KeyDown: + return fyne.KeyDown + case glfw.KeyUp: + return fyne.KeyUp + case glfw.KeyPageUp: + return fyne.KeyPageUp + case glfw.KeyPageDown: + return fyne.KeyPageDown + case glfw.KeyHome: + return fyne.KeyHome + case glfw.KeyEnd: + return fyne.KeyEnd + + case glfw.KeySpace: + return fyne.KeySpace + case glfw.KeyKPEnter: + return fyne.KeyEnter + + // desktop + case glfw.KeyLeftShift: + return desktop.KeyShiftLeft + case glfw.KeyRightShift: + return desktop.KeyShiftRight + case glfw.KeyLeftControl: + return desktop.KeyControlLeft + case glfw.KeyRightControl: + return desktop.KeyControlRight + case glfw.KeyLeftAlt: + return desktop.KeyAltLeft + case glfw.KeyRightAlt: + return desktop.KeyAltRight + case glfw.KeyLeftSuper: + return desktop.KeySuperLeft + case glfw.KeyRightSuper: + return desktop.KeySuperRight + case glfw.KeyMenu: + return desktop.KeyMenu + case glfw.KeyPrintScreen: + return desktop.KeyPrintScreen + case glfw.KeyCapsLock: + return desktop.KeyCapsLock + + // functions + case glfw.KeyF1: + return fyne.KeyF1 + case glfw.KeyF2: + return fyne.KeyF2 + case glfw.KeyF3: + return fyne.KeyF3 + case glfw.KeyF4: + return fyne.KeyF4 + case glfw.KeyF5: + return fyne.KeyF5 + case glfw.KeyF6: + return fyne.KeyF6 + case glfw.KeyF7: + return fyne.KeyF7 + case glfw.KeyF8: + return fyne.KeyF8 + case glfw.KeyF9: + return fyne.KeyF9 + case glfw.KeyF10: + return fyne.KeyF10 + case glfw.KeyF11: + return fyne.KeyF11 + case glfw.KeyF12: + return fyne.KeyF12 + } + + return fyne.KeyUnknown +} + +func keyCodeToKeyName(code string) fyne.KeyName { + if len(code) != 1 { + return fyne.KeyUnknown + } + + char := code[0] + if char >= 'a' && char <= 'z' { + // Our alphabetical keys are all upper case characters. + return fyne.KeyName('A' + char - 'a') + } + + switch char { + case '[': + return fyne.KeyLeftBracket + case '\\': + return fyne.KeyBackslash + case ']': + return fyne.KeyRightBracket + case '\'': + return fyne.KeyApostrophe + case ',': + return fyne.KeyComma + case '-': + return fyne.KeyMinus + case '.': + return fyne.KeyPeriod + case '/': + return fyne.KeySlash + case '*': + return fyne.KeyAsterisk + case '`': + return fyne.KeyBackTick + case ';': + return fyne.KeySemicolon + case '+': + return fyne.KeyPlus + case '=': + return fyne.KeyEqual + } + + return fyne.KeyUnknown +} + +func keyToName(code glfw.Key, scancode int) fyne.KeyName { + ret := glfwKeyToKeyName(code) + if ret != fyne.KeyUnknown { + return ret + } + + // keyName := glfw.GetKeyName(code, scancode) + // return keyCodeToKeyName(keyName) + return fyne.KeyUnknown +} + +func convertAction(action glfw.Action) action { + switch action { + case glfw.Press: + return press + case glfw.Release: + return release + case glfw.Repeat: + return repeat + } + panic("Could not convert glfw.Action.") +} + +func convertASCII(key glfw.Key) fyne.KeyName { + if key < glfw.KeyA || key > glfw.KeyZ { + return fyne.KeyUnknown + } + + return fyne.KeyName(rune(key)) +} + +func (w *window) keyPressed(viewport *glfw.Window, key glfw.Key, scancode int, action glfw.Action, mods glfw.ModifierKey) { + keyName := keyToName(key, scancode) + keyDesktopModifier := desktopModifier(mods) + keyAction := convertAction(action) + keyASCII := convertASCII(key) + + w.processKeyPressed(keyName, keyASCII, scancode, keyAction, keyDesktopModifier) +} + +func desktopModifier(mods glfw.ModifierKey) fyne.KeyModifier { + var m fyne.KeyModifier + if (mods & glfw.ModShift) != 0 { + m |= fyne.KeyModifierShift + } + if (mods & glfw.ModControl) != 0 { + m |= fyne.KeyModifierControl + } + if (mods & glfw.ModAlt) != 0 { + m |= fyne.KeyModifierAlt + } + if (mods & glfw.ModSuper) != 0 { + m |= fyne.KeyModifierSuper + } + return m +} + +// charInput defines the character with modifiers callback which is called when a +// Unicode character is input regardless of what modifier keys are used. +// +// Characters do not map 1:1 to physical keys, as a key may produce zero, one or more characters. +func (w *window) charInput(viewport *glfw.Window, char rune) { + w.processCharInput(char) +} + +func (w *window) focused(_ *glfw.Window, focused bool) { + w.processFocused(focused) +} + +func (w *window) DetachCurrentContext() { + glfw.DetachCurrentContext() +} + +func (w *window) RescaleContext() { + if w.viewport == nil { + return + } + + w.width, w.height = w.viewport.GetSize() + scaledFull := fyne.NewSize( + scale.ToFyneCoordinate(w.canvas, w.width), + scale.ToFyneCoordinate(w.canvas, w.height)) + w.canvas.Resize(scaledFull) + + // Ensure textures re-rasterize at the new scale + cache.DeleteTextTexturesFor(w.canvas) + w.canvas.content.Refresh() +} + +func (w *window) create() { + // we can't hide the window in webgl, so there might be some artifact + initWindowHints() + + pixWidth, pixHeight := w.screenSize(w.canvas.size) + pixWidth = int(fyne.Max(float32(pixWidth), float32(w.width))) + if pixWidth == 0 { + pixWidth = 10 + } + pixHeight = int(fyne.Max(float32(pixHeight), float32(w.height))) + if pixHeight == 0 { + pixHeight = 10 + } + + win, err := glfw.CreateWindow(pixWidth, pixHeight, w.title, nil, nil) + if err != nil { + w.driver.initFailed("window creation error", err) + return + } + + w.viewport = win + + if w.view() == nil { // something went wrong above, it will have been logged + return + } + + // run the GL init on the draw thread + w.RunWithContext(func() { + w.canvas.SetPainter(gl.NewPainter(w.canvas, w)) + w.canvas.Painter().Init() + }) + + w.setDarkMode() + + win.SetCloseCallback(w.closed) + win.SetPosCallback(w.moved) + win.SetSizeCallback(w.resized) + win.SetFramebufferSizeCallback(w.frameSized) + win.SetRefreshCallback(w.refresh) + win.SetCursorPosCallback(w.mouseMoved) + win.SetMouseButtonCallback(w.mouseClicked) + win.SetScrollCallback(w.mouseScrolled) + win.SetKeyCallback(w.keyPressed) + win.SetCharCallback(w.charInput) + win.SetFocusCallback(w.focused) + + w.canvas.detectedScale = w.detectScale() + w.canvas.scale = w.calculatedScale() + w.canvas.texScale = w.detectTextureScale() + // update window size now we have scaled detected + w.fitContent() + + w.drainPendingEvents() + + w.requestedWidth, w.requestedHeight = w.width, w.height + + width, height := win.GetSize() + w.processFrameSized(width, height) + w.processResized(width, height) +} + +func (w *window) view() *glfw.Window { + if w.closing { + return nil + } + return w.viewport +} + +// wrapInner represents a window that is provided by an InnerWindow container in the canvas. +type wrapInner struct { + fyne.Window + inner *container.InnerWindow + d *gLDriver + + centered bool + onClosed func() +} + +func wrapInnerWindow(w *container.InnerWindow, root fyne.Window, d *gLDriver) fyne.Window { + wrapped := &wrapInner{inner: w, d: d} + wrapped.Window = root + w.CloseIntercept = wrapped.doClose + return wrapped +} + +func (w *wrapInner) CenterOnScreen() { + w.centered = true + + w.doCenter() +} + +func (w *wrapInner) Close() { + w.inner.Close() +} + +func (w *wrapInner) Hide() { + w.inner.Hide() + w.updateVisibility() +} + +func (w *wrapInner) Move(p fyne.Position) { + w.inner.Move(p) +} + +func (w *wrapInner) Resize(s fyne.Size) { + w.inner.Resize(s) +} + +func (w *wrapInner) SetContent(o fyne.CanvasObject) { + w.inner.SetContent(o) +} + +func (w *wrapInner) SetOnClosed(fn func()) { + w.onClosed = fn +} + +func (w *wrapInner) Show() { + c := w.Window.Canvas().(*glCanvas) + multi := c.webExtraWindows + multi.Show() + w.inner.Show() + + c.Overlays().Add(multi) + + if w.centered { + w.doCenter() + } +} + +func (w *wrapInner) doCenter() { + c := w.Window.Canvas().(*glCanvas) + multi := c.webExtraWindows + + min := w.inner.MinSize() + min = min.Max(w.inner.Size()) + + x := (multi.Size().Width - min.Width) / 2 + y := (multi.Size().Height - min.Height) / 2 + + w.inner.Move(fyne.NewPos(x, y)) +} + +func (w *wrapInner) doClose() { + c := w.Window.Canvas().(*glCanvas) + multi := c.webExtraWindows + + pos := -1 + for i, child := range multi.Windows { + if child == w.inner { + pos = i + w.inner.Hide() + break + } + } + if pos != -1 { + count := len(multi.Windows) + copy(multi.Windows[pos:], multi.Windows[pos+1:]) + multi.Windows[count-1] = nil + multi.Windows = multi.Windows[:count-1] + } + + if w.onClosed != nil { + w.onClosed() + } + w.updateVisibility() +} + +func (w *wrapInner) updateVisibility() { + c := w.Window.Canvas().(*glCanvas) + multi := c.webExtraWindows + + visible := 0 + for _, win := range multi.Windows { + if win.Visible() { + visible++ + } + } + + if visible > 0 { + multi.Refresh() + } else { + multi.Hide() + c.Overlays().Remove(multi) + } +} + +func (w *window) scaleInput(in float64) float64 { + return in +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/window_wayland.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/window_wayland.go new file mode 100644 index 0000000..8194ef5 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/window_wayland.go @@ -0,0 +1,21 @@ +//go:build wayland && (linux || freebsd || openbsd || netbsd) + +package glfw + +import ( + "unsafe" + + "fyne.io/fyne/v2/driver" +) + +// assert we are implementing driver.NativeWindow +var _ driver.NativeWindow = (*window)(nil) + +func (w *window) RunNative(f func(any)) { + context := driver.WaylandWindowContext{} + if v := w.view(); v != nil { + context.WaylandSurface = uintptr(unsafe.Pointer(v.GetWaylandWindow())) + } + + f(context) +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/window_windows.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/window_windows.go new file mode 100644 index 0000000..0b8e99d --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/window_windows.go @@ -0,0 +1,69 @@ +package glfw + +import ( + "runtime" + "syscall" + "unsafe" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/driver" + "fyne.io/fyne/v2/internal/scale" + + "golang.org/x/sys/windows/registry" +) + +func (w *window) setDarkMode() { + if runtime.GOOS == "windows" { + hwnd := w.view().GetWin32Window() + dark := isDark() + // cannot use a go bool. + var winBool int32 + if dark { + winBool = 1 + } + dwm := syscall.NewLazyDLL("dwmapi.dll") + setAtt := dwm.NewProc("DwmSetWindowAttribute") + ret, _, err := setAtt.Call(uintptr(unsafe.Pointer(hwnd)), // window handle + 20, // DWMWA_USE_IMMERSIVE_DARK_MODE + uintptr(unsafe.Pointer(&winBool)), // on or off + 4) // sizeof(bool for windows) + + if ret != 0 && ret != 0x80070057 { // err is always non-nil, we check return value (except erroneous code) + fyne.LogError("Failed to set dark mode", err) + } + } +} + +func isDark() bool { + k, err := registry.OpenKey(registry.CURRENT_USER, `SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize`, registry.QUERY_VALUE) + if err != nil { // older version of Windows will not have this key + return false + } + defer k.Close() + + useLight, _, err := k.GetIntegerValue("AppsUseLightTheme") + if err != nil { // older version of Windows will not have this value + return false + } + + return useLight == 0 +} + +func (w *window) computeCanvasSize(width, height int) fyne.Size { + if w.fixedSize { + return fyne.NewSize(scale.ToFyneCoordinate(w.canvas, w.width), scale.ToFyneCoordinate(w.canvas, w.height)) + } + return fyne.NewSize(scale.ToFyneCoordinate(w.canvas, width), scale.ToFyneCoordinate(w.canvas, height)) +} + +// assert we are implementing driver.NativeWindow +var _ driver.NativeWindow = (*window)(nil) + +func (w *window) RunNative(f func(any)) { + context := driver.WindowsWindowContext{} + if v := w.view(); v != nil { + context.HWND = uintptr(unsafe.Pointer(v.GetWin32Window())) + } + + f(context) +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/window_x11.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/window_x11.go new file mode 100644 index 0000000..76cb4d9 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/window_x11.go @@ -0,0 +1,17 @@ +//go:build !wayland && (linux || freebsd || openbsd || netbsd) && !wasm && !test_web_driver + +package glfw + +import "fyne.io/fyne/v2/driver" + +// assert we are implementing driver.NativeWindow +var _ driver.NativeWindow = (*window)(nil) + +func (w *window) RunNative(f func(any)) { + context := driver.X11WindowContext{} + if v := w.view(); v != nil { + context.WindowHandle = uintptr(v.GetX11Window()) + } + + f(context) +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/glfw/window_xdg.go b/vendor/fyne.io/fyne/v2/internal/driver/glfw/window_xdg.go new file mode 100644 index 0000000..1c5c720 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/glfw/window_xdg.go @@ -0,0 +1,9 @@ +//go:build linux || freebsd || openbsd || netbsd + +package glfw + +import "fyne.io/fyne/v2" + +func (w *window) platformResize(canvasSize fyne.Size) { + w.canvas.Resize(canvasSize) +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/README.txt b/vendor/fyne.io/fyne/v2/internal/driver/mobile/README.txt new file mode 100644 index 0000000..2ae9e08 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/README.txt @@ -0,0 +1,7 @@ +This directory is a fork of the golang.org/x/mobile package. It has largely +deviated from the original package to better support fyne. + +The full project, its license can be found at https://github.com/golang/mobile + +This package is for the purpose of removing the dependency of mobile drivers +and will be removed in due course. \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/android.c b/vendor/fyne.io/fyne/v2/internal/driver/mobile/android.c new file mode 100644 index 0000000..db4f55c --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/android.c @@ -0,0 +1,533 @@ +//go:build android + +#include +#include +#include +#include +#include +#include +#include + +#define LOG_FATAL(...) __android_log_print(ANDROID_LOG_FATAL, "Fyne", __VA_ARGS__) + +static jclass find_class(JNIEnv *env, const char *class_name) { + jclass clazz = (*env)->FindClass(env, class_name); + if (clazz == NULL) { + (*env)->ExceptionClear(env); + LOG_FATAL("cannot find %s", class_name); + return NULL; + } + return clazz; +} + +static jmethodID find_method(JNIEnv *env, jclass clazz, const char *name, const char *sig) { + jmethodID m = (*env)->GetMethodID(env, clazz, name, sig); + if (m == 0) { + (*env)->ExceptionClear(env); + LOG_FATAL("cannot find method %s %s", name, sig); + return 0; + } + return m; +} + +static jmethodID find_static_method(JNIEnv *env, jclass clazz, const char *name, const char *sig) { + jmethodID m = (*env)->GetStaticMethodID(env, clazz, name, sig); + if (m == 0) { + (*env)->ExceptionClear(env); + LOG_FATAL("cannot find method %s %s", name, sig); + return 0; + } + return m; +} + +const char* getString(uintptr_t jni_env, uintptr_t ctx, jstring str) { + JNIEnv *env = (JNIEnv*)jni_env; + + const char *chars = (*env)->GetStringUTFChars(env, str, NULL); + + const char *copy = strdup(chars); + (*env)->ReleaseStringUTFChars(env, str, chars); + return copy; +} + +jobject parseURI(uintptr_t jni_env, uintptr_t ctx, char* uriCstr) { + JNIEnv *env = (JNIEnv*)jni_env; + + jstring uriStr = (*env)->NewStringUTF(env, uriCstr); + jclass uriClass = find_class(env, "android/net/Uri"); + jmethodID parse = find_static_method(env, uriClass, "parse", "(Ljava/lang/String;)Landroid/net/Uri;"); + + return (jobject)(*env)->CallStaticObjectMethod(env, uriClass, parse, uriStr); +} + +// clipboard + +jobject getClipboard(uintptr_t jni_env, uintptr_t ctx) { + JNIEnv *env = (JNIEnv*)jni_env; + jclass ctxClass = (*env)->GetObjectClass(env, (jobject)ctx); + jmethodID getSystemService = find_method(env, ctxClass, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;"); + + jstring service = (*env)->NewStringUTF(env, "clipboard"); + jobject ret = (*env)->CallObjectMethod(env, (jobject)ctx, getSystemService, service); + jthrowable err = (*env)->ExceptionOccurred(env); + + if (err != NULL) { + LOG_FATAL("cannot lookup clipboard"); + (*env)->ExceptionClear(env); + return NULL; + } + return ret; +} + +const char *getClipboardContent(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx) { + JNIEnv *env = (JNIEnv*)jni_env; + jobject mgr = getClipboard(jni_env, ctx); + if (mgr == NULL) { + return NULL; + } + + jclass mgrClass = (*env)->GetObjectClass(env, mgr); + jmethodID getText = find_method(env, mgrClass, "getText", "()Ljava/lang/CharSequence;"); + + jobject content = (jstring)(*env)->CallObjectMethod(env, mgr, getText); + if (content == NULL) { + return NULL; + } + + jclass clzCharSequence = (*env)->GetObjectClass(env, content); + jmethodID toString = (*env)->GetMethodID(env, clzCharSequence, "toString", "()Ljava/lang/String;"); + jobject s = (*env)->CallObjectMethod(env, content, toString); + + return getString(jni_env, ctx, s); +} + +void setClipboardContent(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx, char *content) { + JNIEnv *env = (JNIEnv*)jni_env; + jobject mgr = getClipboard(jni_env, ctx); + if (mgr == NULL) { + return; + } + + jclass mgrClass = (*env)->GetObjectClass(env, mgr); + jmethodID setText = find_method(env, mgrClass, "setText", "(Ljava/lang/CharSequence;)V"); + + jstring str = (*env)->NewStringUTF(env, content); + (*env)->CallVoidMethod(env, mgr, setText, str); +} + +// file handling + +jobject getContentResolver(uintptr_t jni_env, uintptr_t ctx) { + JNIEnv *env = (JNIEnv*)jni_env; + jclass ctxClass = (*env)->GetObjectClass(env, (jobject)ctx); + jmethodID getContentResolver = find_method(env, ctxClass, "getContentResolver", "()Landroid/content/ContentResolver;"); + + return (jobject)(*env)->CallObjectMethod(env, (jobject)ctx, getContentResolver); +} + +void* openStream(uintptr_t jni_env, uintptr_t ctx, char* uriCstr) { + JNIEnv *env = (JNIEnv*)jni_env; + jobject resolver = getContentResolver(jni_env, ctx); + + jclass resolverClass = (*env)->GetObjectClass(env, resolver); + jmethodID openInputStream = find_method(env, resolverClass, "openInputStream", "(Landroid/net/Uri;)Ljava/io/InputStream;"); + + jobject uri = parseURI(jni_env, ctx, uriCstr); + jobject stream = (jobject)(*env)->CallObjectMethod(env, resolver, openInputStream, uri); + jthrowable loadErr = (*env)->ExceptionOccurred(env); + + if (loadErr != NULL) { + (*env)->ExceptionClear(env); + return NULL; + } + + return (*env)->NewGlobalRef(env, stream); +} + +void* saveStream(uintptr_t jni_env, uintptr_t ctx, char* uriCstr, bool truncate) { + JNIEnv *env = (JNIEnv*)jni_env; + jobject resolver = getContentResolver(jni_env, ctx); + + jclass resolverClass = (*env)->GetObjectClass(env, resolver); + jmethodID saveOutputStream = find_method(env, resolverClass, "openOutputStream", "(Landroid/net/Uri;Ljava/lang/String;)Ljava/io/OutputStream;"); + + jobject uri = parseURI(jni_env, ctx, uriCstr); + jstring modes = NULL; + if (truncate) { + modes = (*env)->NewStringUTF(env, "wt"); // truncate before write + } else { + modes = (*env)->NewStringUTF(env, "wa"); + } + jobject stream = (jobject)(*env)->CallObjectMethod(env, resolver, saveOutputStream, uri, modes); + jthrowable loadErr = (*env)->ExceptionOccurred(env); + + if (loadErr != NULL) { + (*env)->ExceptionClear(env); + return NULL; + } + + return (*env)->NewGlobalRef(env, stream); +} + +jbyte* readStream(uintptr_t jni_env, uintptr_t ctx, void* stream, int len, int* total) { + JNIEnv *env = (JNIEnv*)jni_env; + jclass streamClass = (*env)->GetObjectClass(env, stream); + jmethodID read = find_method(env, streamClass, "read", "([BII)I"); + + jbyteArray data = (*env)->NewByteArray(env, len); + int count = (int)(*env)->CallIntMethod(env, stream, read, data, 0, len); + *total = count; + + if (count == -1) { + return NULL; + } + + jbyte* bytes = (jbyte*)malloc(sizeof(jbyte)*count); + (*env)->GetByteArrayRegion(env, data, 0, count, bytes); + return bytes; +} + +void writeStream(uintptr_t jni_env, uintptr_t ctx, void* stream, jbyte* buf, int len) { + JNIEnv *env = (JNIEnv*)jni_env; + jclass streamClass = (*env)->GetObjectClass(env, stream); + jmethodID write = find_method(env, streamClass, "write", "([BII)V"); + + jbyteArray data = (*env)->NewByteArray(env, len); + (*env)->SetByteArrayRegion(env, data, 0, len, buf); + + (*env)->CallVoidMethod(env, stream, write, data, 0, len); + + free(buf); +} + +void closeStream(uintptr_t jni_env, uintptr_t ctx, void* stream) { + JNIEnv *env = (JNIEnv*)jni_env; + jclass streamClass = (*env)->GetObjectClass(env, stream); + jmethodID close = find_method(env, streamClass, "close", "()V"); + (*env)->CallVoidMethod(env, stream, close); + + (*env)->DeleteGlobalRef(env, stream); +} + +bool hasPrefix(char* string, char* prefix) { + size_t lp = strlen(prefix); + size_t ls = strlen(string); + if (ls < lp) { + return false; + } + return memcmp(prefix, string, lp) == 0; +} + +bool canListContentURI(uintptr_t jni_env, uintptr_t ctx, char* uriCstr) { + JNIEnv *env = (JNIEnv*)jni_env; + jobject resolver = getContentResolver(jni_env, ctx); + jobject uri = parseURI(jni_env, ctx, uriCstr); + jthrowable loadErr = (*env)->ExceptionOccurred(env); + + if (loadErr != NULL) { + (*env)->ExceptionClear(env); + return false; + } + + jclass contractClass = find_class(env, "android/provider/DocumentsContract"); + if (contractClass == NULL) { // API 19 + return false; + } + jmethodID getDoc = find_static_method(env, contractClass, "getTreeDocumentId", "(Landroid/net/Uri;)Ljava/lang/String;"); + if (getDoc == NULL) { // API 21 + return false; + } + jstring docID = (jobject)(*env)->CallStaticObjectMethod(env, contractClass, getDoc, uri); + + jmethodID getTree = find_static_method(env, contractClass, "buildDocumentUriUsingTree", "(Landroid/net/Uri;Ljava/lang/String;)Landroid/net/Uri;"); + jobject treeUri = (jobject)(*env)->CallStaticObjectMethod(env, contractClass, getTree, uri, docID); + + jclass resolverClass = (*env)->GetObjectClass(env, resolver); + jmethodID getType = find_method(env, resolverClass, "getType", "(Landroid/net/Uri;)Ljava/lang/String;"); + jstring type = (jstring)(*env)->CallObjectMethod(env, resolver, getType, treeUri); + + if (type == NULL) { + return false; + } + + const char *str = getString(jni_env, ctx, type); + return strcmp(str, "vnd.android.document/directory") == 0; +} + +bool canListFileURI(char* uriCstr) { + // Get file path from URI + size_t length = strlen(uriCstr)-7;// -7 for 'file://' + char* path = malloc(sizeof(char)*(length+1));// +1 for '\0' + memcpy(path, &uriCstr[7], length); + path[length] = '\0'; + + // Stat path to determine if it points to a directory + struct stat statbuf; + int result = stat(path, &statbuf); + + free(path); + + return (result == 0) && S_ISDIR(statbuf.st_mode); +} + +bool canListURI(uintptr_t jni_env, uintptr_t ctx, char* uriCstr) { + if (hasPrefix(uriCstr, "file://")) { + return canListFileURI(uriCstr); + } else if (hasPrefix(uriCstr, "content://")) { + return canListContentURI(jni_env, ctx, uriCstr); + } + LOG_FATAL("Unrecognized scheme: %s", uriCstr); + return false; +} + +bool createListableFileURI(char* uriCstr) { + // Get file path from URI + size_t length = strlen(uriCstr)-7;// -7 for 'file://' + char* path = malloc(sizeof(char)*(length+1));// +1 for '\0' + memcpy(path, &uriCstr[7], length); + path[length] = '\0'; + + int result = mkdir(path, S_IRWXU); + free(path); + + return result == 0; +} + +bool createListableURI(uintptr_t jni_env, uintptr_t ctx, char* uriCstr) { + if (hasPrefix(uriCstr, "file://")) { + return createListableFileURI(uriCstr); + } + LOG_FATAL("Cannot create directory for scheme: %s", uriCstr); + return false; +} + +const char* contentURIGetFileName(uintptr_t jni_env, uintptr_t ctx, char* uriCstr) { + JNIEnv *env = (JNIEnv*)jni_env; + jobject resolver = getContentResolver(jni_env, ctx); + jobject uri = parseURI(jni_env, ctx, uriCstr); + jthrowable loadErr = (*env)->ExceptionOccurred(env); + + if (loadErr != NULL) { + (*env)->ExceptionClear(env); + return ""; + } + + jclass stringClass = find_class(env, "java/lang/String"); + jobjectArray project = (*env)->NewObjectArray(env, 1, stringClass, (*env)->NewStringUTF(env, "_display_name")); + + jclass resolverClass = (*env)->GetObjectClass(env, resolver); + jmethodID query = find_method(env, resolverClass, "query", "(Landroid/net/Uri;[Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;)Landroid/database/Cursor;"); + + jobject cursor = (jobject)(*env)->CallObjectMethod(env, resolver, query, uri, project, NULL, NULL, NULL); + jclass cursorClass = (*env)->GetObjectClass(env, cursor); + + jmethodID first = find_method(env, cursorClass, "moveToFirst", "()Z"); + jmethodID get = find_method(env, cursorClass, "getString", "(I)Ljava/lang/String;"); + + if (((jboolean)(*env)->CallBooleanMethod(env, cursor, first)) == JNI_TRUE) { + jstring name = (jstring)(*env)->CallObjectMethod(env, cursor, get, 0); + const char *fname = getString(jni_env, ctx, name); + return fname; + } + + return NULL; +} + +char *filePath(char *uriCstr) { + // Get file path from URI + size_t length = strlen(uriCstr)-7;// -7 for 'file://' + char* path = malloc(sizeof(char)*(length+1));// +1 for '\0' + memcpy(path, &uriCstr[7], length); + path[length] = '\0'; + + return path; +} + +bool deleteFileURI(char *uriCstr) { + char* path = filePath(uriCstr); + int result = remove(path); + + free(path); + + return result == 0; +} + +bool deleteURI(uintptr_t jni_env, uintptr_t ctx, char* uriCstr) { + if (!hasPrefix(uriCstr, "file://")) { + LOG_FATAL("Cannot delete for scheme: %s", uriCstr); + return false; + } + + return deleteFileURI(uriCstr); +} + +bool existsFileURI(char* uriCstr) { + char* path = filePath(uriCstr); + + // Stat path to determine if it points to an existing file + struct stat statbuf; + int result = stat(path, &statbuf); + + free(path); + + return result == 0; +} + +bool existsURI(uintptr_t jni_env, uintptr_t ctx, char* uriCstr) { + if (hasPrefix(uriCstr, "file://")) { + return existsFileURI(uriCstr); + } + LOG_FATAL("Cannot check exists for scheme: %s", uriCstr); + return false; +} + +char* listContentURI(uintptr_t jni_env, uintptr_t ctx, char* uriCstr) { + JNIEnv *env = (JNIEnv*)jni_env; + jobject resolver = getContentResolver(jni_env, ctx); + jobject uri = parseURI(jni_env, ctx, uriCstr); + jthrowable loadErr = (*env)->ExceptionOccurred(env); + + if (loadErr != NULL) { + (*env)->ExceptionClear(env); + return ""; + } + + jclass contractClass = find_class(env, "android/provider/DocumentsContract"); + if (contractClass == NULL) { // API 19 + return ""; + } + jmethodID getDoc = find_static_method(env, contractClass, "getTreeDocumentId", "(Landroid/net/Uri;)Ljava/lang/String;"); + if (getDoc == NULL) { // API 21 + return ""; + } + jstring docID = (jobject)(*env)->CallStaticObjectMethod(env, contractClass, getDoc, uri); + + jmethodID getChild = find_static_method(env, contractClass, "buildChildDocumentsUriUsingTree", "(Landroid/net/Uri;Ljava/lang/String;)Landroid/net/Uri;"); + jobject childrenUri = (jobject)(*env)->CallStaticObjectMethod(env, contractClass, getChild, uri, docID); + + jclass stringClass = find_class(env, "java/lang/String"); + jobjectArray project = (*env)->NewObjectArray(env, 1, stringClass, (*env)->NewStringUTF(env, "document_id")); + + jclass resolverClass = (*env)->GetObjectClass(env, resolver); + jmethodID query = find_method(env, resolverClass, "query", "(Landroid/net/Uri;[Ljava/lang/String;Landroid/os/Bundle;Landroid/os/CancellationSignal;)Landroid/database/Cursor;"); + if (getDoc == NULL) { // API 26 + return ""; + } + + jobject cursor = (jobject)(*env)->CallObjectMethod(env, resolver, query, childrenUri, project, NULL, NULL); + jclass cursorClass = (*env)->GetObjectClass(env, cursor); + jmethodID next = find_method(env, cursorClass, "moveToNext", "()Z"); + jmethodID get = find_method(env, cursorClass, "getString", "(I)Ljava/lang/String;"); + jmethodID getChildURI = find_static_method(env, contractClass, "buildDocumentUriUsingTree", "(Landroid/net/Uri;Ljava/lang/String;)Landroid/net/Uri;"); + + char *ret = NULL; + int len = 0; + while (((jboolean)(*env)->CallBooleanMethod(env, cursor, next)) == JNI_TRUE) { + jstring childDocId = (jstring)(*env)->CallObjectMethod(env, cursor, get, 0); + jobject childUri = (jobject)(*env)->CallStaticObjectMethod(env, contractClass, getChildURI, uri, childDocId); + jclass uriClass = (*env)->GetObjectClass(env, childUri); + jmethodID toString = (*env)->GetMethodID(env, uriClass, "toString", "()Ljava/lang/String;"); + jstring s = (jstring)(*env)->CallObjectMethod(env, childUri, toString); + + const char *uid = getString(jni_env, ctx, s); + + // append + char *old = ret; + len = len + strlen(uid) + 1; + ret = malloc(sizeof(char)*(len+1)); + if (old != NULL) { + strcpy(ret, old); + free(old); + } else { + ret[0] = '\0'; + } + strcat(ret, uid); + strcat(ret, "|"); + } + + if (ret != NULL) { + ret[len-1] = '\0'; + } + return ret; +} + +char* listFileURI(char* uriCstr) { + + size_t uriLength = strlen(uriCstr); + + // Get file path from URI + size_t length = uriLength-7;// -7 for 'file://' + char* path = malloc(sizeof(char)*(length+1));// +1 for '\0' + memcpy(path, &uriCstr[7], length); + path[length] = '\0'; + + char *ret = NULL; + DIR *dfd; + if ((dfd = opendir(path)) != NULL) { + struct dirent *dp; + int len = 0; + while ((dp = readdir(dfd)) != NULL) { + if (strcmp(dp->d_name, ".") == 0) { + // Ignore current directory + continue; + } + if (strcmp(dp->d_name, "..") == 0) { + // Ignore parent directory + continue; + } + // append + char *old = ret; + len = len + uriLength + 1 /* / */ + strlen(dp->d_name) + 1 /* | */; + ret = malloc(sizeof(char)*(len+1)); + if (old != NULL) { + strcpy(ret, old); + free(old); + } else { + ret[0] = '\0'; + } + strcat(ret, uriCstr); + strcat(ret, "/"); + strcat(ret, dp->d_name); + strcat(ret, "|"); + } + if (ret != NULL) { + ret[len-1] = '\0'; + } + } + + free(path); + + return ret; +} + +char* listURI(uintptr_t jni_env, uintptr_t ctx, char* uriCstr) { + if (hasPrefix(uriCstr, "file://")) { + return listFileURI(uriCstr); + } else if (hasPrefix(uriCstr, "content://")) { + return listContentURI(jni_env, ctx, uriCstr); + } + LOG_FATAL("Unrecognized scheme: %s", uriCstr); + return ""; +} + +void keepScreenOn(uintptr_t jni_env, uintptr_t ctx, bool disabled) { + JNIEnv *env = (JNIEnv*)jni_env; + jclass activityClass = find_class(env, "android/app/Activity"); + jmethodID getWindow = find_method(env, activityClass, "getWindow", "()Landroid/view/Window;"); + + jobject win = (*env)->CallObjectMethod(env, (jobject)ctx, getWindow); + jclass windowClass = find_class(env, "android/view/Window"); + + jmethodID action = NULL; + if (disabled) { + action = find_method(env, windowClass, "addFlags", "(I)V"); + } else { + action = find_method(env, windowClass, "clearFlags", "(I)V"); + } + + jclass paramsClass = find_class(env, "android/view/WindowManager$LayoutParams" ); + jfieldID screenFlagField = (*env)->GetStaticFieldID(env, paramsClass, "FLAG_KEEP_SCREEN_ON", "I" ); + int screenFlag = (*env)->GetStaticIntField(env, paramsClass, screenFlagField); + + (*env)->CallVoidMethod(env, win, action, screenFlag); +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/animation.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/animation.go new file mode 100644 index 0000000..aab26dd --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/animation.go @@ -0,0 +1,11 @@ +package mobile + +import "fyne.io/fyne/v2" + +func (d *driver) StartAnimation(a *fyne.Animation) { + d.animation.Start(a) +} + +func (d *driver) StopAnimation(a *fyne.Animation) { + d.animation.Stop(a) +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/GoNativeActivity.java b/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/GoNativeActivity.java new file mode 100644 index 0000000..f324ef4 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/GoNativeActivity.java @@ -0,0 +1,356 @@ +package org.golang.app; + +import android.app.Activity; +import android.app.NativeActivity; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.text.Editable; +import android.text.InputType; +import android.text.TextWatcher; +import android.text.method.DigitsKeyListener; +import android.util.Log; +import android.view.Gravity; +import android.view.KeyCharacterMap; +import android.view.View; +import android.view.WindowInsets; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.view.KeyEvent; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.TextView; +import android.widget.TextView.OnEditorActionListener; + +public class GoNativeActivity extends NativeActivity { + private static GoNativeActivity goNativeActivity; + private static final int FILE_OPEN_CODE = 1; + private static final int FILE_SAVE_CODE = 2; + + private static final int DEFAULT_INPUT_TYPE = InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS; + + private static final int DEFAULT_KEYBOARD_CODE = 0; + private static final int SINGLELINE_KEYBOARD_CODE = 1; + private static final int NUMBER_KEYBOARD_CODE = 2; + private static final int PASSWORD_KEYBOARD_CODE = 3; + + private native void filePickerReturned(String str); + private native void insetsChanged(int top, int bottom, int left, int right); + private native void keyboardTyped(String str); + private native void keyboardDelete(); + private native void backPressed(); + private native void setDarkMode(boolean dark); + + private EditText mTextEdit; + private boolean ignoreKey = false; + private boolean keyboardUp = false; + + public GoNativeActivity() { + super(); + goNativeActivity = this; + } + + String getTmpdir() { + return getCacheDir().getAbsolutePath(); + } + + void updateLayout() { + try { + WindowInsets insets = getWindow().getDecorView().getRootWindowInsets(); + if (insets == null) { + return; + } + + insetsChanged(insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetBottom(), + insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetRight()); + } catch (java.lang.NoSuchMethodError e) { + Rect insets = new Rect(); + getWindow().getDecorView().getWindowVisibleDisplayFrame(insets); + + View view = findViewById(android.R.id.content).getRootView(); + insetsChanged(insets.top, view.getHeight() - insets.height() - insets.top, + insets.left, view.getWidth() - insets.width() - insets.left); + } + } + + static void showKeyboard(int keyboardType) { + goNativeActivity.doShowKeyboard(keyboardType); + goNativeActivity.keyboardUp = true; + } + + void doShowKeyboard(final int keyboardType) { + runOnUiThread(new Runnable() { + @Override + public void run() { + int imeOptions = EditorInfo.IME_FLAG_NO_ENTER_ACTION; + int inputType = DEFAULT_INPUT_TYPE; + String keys = ""; + switch (keyboardType) { + case DEFAULT_KEYBOARD_CODE: + imeOptions = EditorInfo.IME_FLAG_NO_ENTER_ACTION; + break; + case SINGLELINE_KEYBOARD_CODE: + imeOptions = EditorInfo.IME_ACTION_DONE; + break; + case NUMBER_KEYBOARD_CODE: + imeOptions = EditorInfo.IME_ACTION_DONE; + inputType |= InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_NORMAL; + keys = "0123456789.,-' "; // work around android bug where some number keys are blocked + break; + case PASSWORD_KEYBOARD_CODE: + imeOptions = EditorInfo.IME_ACTION_DONE; + inputType |= InputType.TYPE_TEXT_VARIATION_PASSWORD; + default: + Log.e("Fyne", "unknown keyboard type, use default"); + } + mTextEdit.setImeOptions(imeOptions|EditorInfo.IME_FLAG_NO_FULLSCREEN); + mTextEdit.setInputType(inputType); + if (keys != "") { + mTextEdit.setKeyListener(DigitsKeyListener.getInstance(keys)); + } + + mTextEdit.setOnEditorActionListener(new OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (actionId == EditorInfo.IME_ACTION_DONE) { + keyboardTyped("\n"); + } + return false; + } + }); + + // always place one character so all keyboards can send backspace + ignoreKey = true; + mTextEdit.setText(" "); + mTextEdit.setSelection(mTextEdit.getText().length()); + ignoreKey = false; + + mTextEdit.setVisibility(View.VISIBLE); + mTextEdit.bringToFront(); + mTextEdit.requestFocus(); + + InputMethodManager m = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + m.showSoftInput(mTextEdit, 0); + } + }); + } + + static void hideKeyboard() { + goNativeActivity.doHideKeyboard(); + goNativeActivity.keyboardUp = false; + } + + void doHideKeyboard() { + InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + View view = findViewById(android.R.id.content).getRootView(); + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + + runOnUiThread(new Runnable() { + @Override + public void run() { + mTextEdit.setVisibility(View.GONE); + } + }); + } + + static void showFileOpen(String mimes) { + goNativeActivity.doShowFileOpen(mimes); + } + + void doShowFileOpen(String mimes) { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + if ("application/x-directory".equals(mimes) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); // ask for a directory picker if OS supports it + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + } else if (mimes.contains("|") && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + intent.setType("*/*"); + intent.putExtra(Intent.EXTRA_MIME_TYPES, mimes.split("\\|")); + intent.addCategory(Intent.CATEGORY_OPENABLE); + } else { + intent.setType(mimes); + intent.addCategory(Intent.CATEGORY_OPENABLE); + } + startActivityForResult(Intent.createChooser(intent, "Open File"), FILE_OPEN_CODE); + } + + static void showFileSave(String mimes, String filename) { + goNativeActivity.doShowFileSave(mimes, filename); + } + + void doShowFileSave(String mimes, String filename) { + Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + if (mimes.contains("|") && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + intent.setType("*/*"); + intent.putExtra(Intent.EXTRA_MIME_TYPES, mimes.split("\\|")); + } else { + intent.setType(mimes); + } + intent.putExtra(Intent.EXTRA_TITLE, filename); + intent.addCategory(Intent.CATEGORY_OPENABLE); + startActivityForResult(Intent.createChooser(intent, "Save File"), FILE_SAVE_CODE); + } + static int getRune(int deviceId, int keyCode, int metaState) { + try { + int rune = KeyCharacterMap.load(deviceId).get(keyCode, metaState); + if (rune == 0) { + return -1; + } + return rune; + } catch (KeyCharacterMap.UnavailableException e) { + return -1; + } catch (Exception e) { + Log.e("Fyne", "exception reading KeyCharacterMap", e); + return -1; + } + } + + private void load() { + // Interestingly, NativeActivity uses a different method + // to find native code to execute, avoiding + // System.loadLibrary. The result is Java methods + // implemented in C with JNIEXPORT (and JNI_OnLoad) are not + // available unless an explicit call to System.loadLibrary + // is done. So we do it here, borrowing the name of the + // library from the same AndroidManifest.xml metadata used + // by NativeActivity. + try { + ActivityInfo ai = getPackageManager().getActivityInfo( + getIntent().getComponent(), PackageManager.GET_META_DATA); + if (ai.metaData == null) { + Log.e("Fyne", "loadLibrary: no manifest metadata found"); + return; + } + String libName = ai.metaData.getString("android.app.lib_name"); + System.loadLibrary(libName); + } catch (Exception e) { + Log.e("Fyne", "loadLibrary failed", e); + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + load(); + super.onCreate(savedInstanceState); + setupEntry(); + updateTheme(getResources().getConfiguration()); + + View view = findViewById(android.R.id.content).getRootView(); + view.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { + public void onLayoutChange (View v, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) { + GoNativeActivity.this.updateLayout(); + } + }); + } + + private void setupEntry() { + runOnUiThread(new Runnable() { + @Override + public void run() { + mTextEdit = new EditText(goNativeActivity); + mTextEdit.setVisibility(View.GONE); + mTextEdit.setInputType(DEFAULT_INPUT_TYPE); + + FrameLayout.LayoutParams mEditTextLayoutParams = new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT); + mTextEdit.setLayoutParams(mEditTextLayoutParams); + addContentView(mTextEdit, mEditTextLayoutParams); + + // always place one character so all keyboards can send backspace + mTextEdit.setText(" "); + mTextEdit.setSelection(mTextEdit.getText().length()); + + mTextEdit.addTextChangedListener(new TextWatcher() { + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + if (ignoreKey) { + return; + } + if (count > 0) { + keyboardTyped(s.subSequence(start,start+count).toString()); + } + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + if (ignoreKey) { + return; + } + if (count > 0) { + for (int i = 0; i < count; i++) { + // send a backspace + keyboardDelete(); + } + } + } + + @Override + public void afterTextChanged(Editable s) { + // always place one character so all keyboards can send backspace + if (s.length() < 1) { + ignoreKey = true; + mTextEdit.setText(" "); + mTextEdit.setSelection(mTextEdit.getText().length()); + ignoreKey = false; + return; + } + } + }); + } + }); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + // unhandled request + if (requestCode != FILE_OPEN_CODE && requestCode != FILE_SAVE_CODE) { + return; + } + + // dialog was cancelled + if (resultCode != Activity.RESULT_OK) { + filePickerReturned(""); + return; + } + + Uri uri = data.getData(); + filePickerReturned(uri.toString()); + } + + @Override + public void onBackPressed() { + if (goNativeActivity.keyboardUp) { + hideKeyboard(); + return; + } + + // skip the default behaviour - we can call finishActivity if we want to go back + backPressed(); + } + + public void finishActivity() { + runOnUiThread(new Runnable() { + @Override + public void run() { + GoNativeActivity.super.onBackPressed(); + } + }); + } + + @Override + public void onConfigurationChanged(Configuration config) { + super.onConfigurationChanged(config); + updateTheme(config); + } + + protected void updateTheme(Configuration config) { + boolean dark = (config.uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; + setDarkMode(dark); + } +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/android.c b/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/android.c new file mode 100644 index 0000000..8980cb2 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/android.c @@ -0,0 +1,307 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build android + +#include +#include +#include +#include +#include +#include +#include +#include "_cgo_export.h" + +#define LOG_INFO(...) __android_log_print(ANDROID_LOG_INFO, "Fyne", __VA_ARGS__) +#define LOG_FATAL(...) __android_log_print(ANDROID_LOG_FATAL, "Fyne", __VA_ARGS__) + +static jclass current_class; + +static jclass find_class(JNIEnv *env, const char *class_name) { + jclass clazz = (*env)->FindClass(env, class_name); + if (clazz == NULL) { + (*env)->ExceptionClear(env); + LOG_FATAL("cannot find %s", class_name); + return NULL; + } + return clazz; +} + +static jmethodID find_method(JNIEnv *env, jclass clazz, const char *name, const char *sig) { + jmethodID m = (*env)->GetMethodID(env, clazz, name, sig); + if (m == 0) { + (*env)->ExceptionClear(env); + LOG_FATAL("cannot find method %s %s", name, sig); + return 0; + } + return m; +} + +static jmethodID find_static_method(JNIEnv *env, jclass clazz, const char *name, const char *sig) { + jmethodID m = (*env)->GetStaticMethodID(env, clazz, name, sig); + if (m == 0) { + (*env)->ExceptionClear(env); + LOG_FATAL("cannot find method %s %s", name, sig); + return 0; + } + return m; +} + +static jmethodID key_rune_method; +static jmethodID show_keyboard_method; +static jmethodID hide_keyboard_method; +static jmethodID show_file_open_method; +static jmethodID show_file_save_method; +static jmethodID finish_method; + +jint JNI_OnLoad(JavaVM* vm, void* reserved) { + JNIEnv* env; + if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { + return -1; + } + + return JNI_VERSION_1_6; +} + +static int main_running = 0; + +// ensure we refresh context on resume in case something has changed... +void onResume(ANativeActivity *activity) { + JNIEnv* env = activity->env; + setCurrentContext(activity->vm, (*env)->NewGlobalRef(env, activity->clazz)); +} + +void onStart(ANativeActivity *activity) {} +void onPause(ANativeActivity *activity) {} +void onStop(ANativeActivity *activity) {} + +// Entry point from our subclassed NativeActivity. +// +// By here, the Go runtime has been initialized (as we are running in +// -buildmode=c-shared) but the first time it is called, Go's main.main +// hasn't been called yet. +// +// The Activity may be created and destroyed multiple times throughout +// the life of a single process. Each time, onCreate is called. +void ANativeActivity_onCreate(ANativeActivity *activity, void* savedState, size_t savedStateSize) { + if (!main_running) { + JNIEnv* env = activity->env; + + // Note that activity->clazz is mis-named. + current_class = (*env)->GetObjectClass(env, activity->clazz); + current_class = (*env)->NewGlobalRef(env, current_class); + key_rune_method = find_static_method(env, current_class, "getRune", "(III)I"); + show_keyboard_method = find_static_method(env, current_class, "showKeyboard", "(I)V"); + hide_keyboard_method = find_static_method(env, current_class, "hideKeyboard", "()V"); + show_file_open_method = find_static_method(env, current_class, "showFileOpen", "(Ljava/lang/String;)V"); + show_file_save_method = find_static_method(env, current_class, "showFileSave", "(Ljava/lang/String;Ljava/lang/String;)V"); + finish_method = find_method(env, current_class, "finishActivity", "()V"); + + setCurrentContext(activity->vm, (*env)->NewGlobalRef(env, activity->clazz)); + + jmethodID getfilesdir = find_method(env, current_class, "getFilesDir", "()Ljava/io/File;"); + jobject filesdirfile = (jobject)(*env)->CallObjectMethod(env, activity->clazz, getfilesdir, NULL); + jclass file_class = (*env)->GetObjectClass(env, filesdirfile); + jmethodID getabsolutepath = find_method(env, file_class, "getAbsolutePath", "()Ljava/lang/String;"); + jstring jpath = (jstring)(*env)->CallObjectMethod(env, filesdirfile, getabsolutepath, NULL); + const char* filesdir = (*env)->GetStringUTFChars(env, jpath, NULL); + + // Set FILESDIR + if (setenv("FILESDIR", filesdir, 1) != 0) { + LOG_INFO("setenv(\"FILESDIR\", \"%s\", 1) failed: %d", activity->internalDataPath, errno); + } + + // Set TMPDIR. + jmethodID gettmpdir = find_method(env, current_class, "getTmpdir", "()Ljava/lang/String;"); + jpath = (jstring)(*env)->CallObjectMethod(env, activity->clazz, gettmpdir, NULL); + const char* tmpdir = (*env)->GetStringUTFChars(env, jpath, NULL); + if (setenv("TMPDIR", tmpdir, 1) != 0) { + LOG_INFO("setenv(\"TMPDIR\", \"%s\", 1) failed: %d", tmpdir, errno); + } + (*env)->ReleaseStringUTFChars(env, jpath, tmpdir); + + // Call the Go main.main. + uintptr_t mainPC = (uintptr_t)dlsym(RTLD_DEFAULT, "main.main"); + if (!mainPC) { + LOG_FATAL("missing main.main"); + } + callMain(mainPC); + main_running = 1; + } + + // These functions match the methods on Activity, described at + // http://developer.android.com/reference/android/app/Activity.html + // + // Note that onNativeWindowResized is not called on resize. Avoid it. + // https://code.google.com/p/android/issues/detail?id=180645 + activity->callbacks->onStart = onStart; + activity->callbacks->onResume = onResume; + activity->callbacks->onSaveInstanceState = onSaveInstanceState; + activity->callbacks->onPause = onPause; + activity->callbacks->onStop = onStop; + activity->callbacks->onDestroy = onDestroy; + activity->callbacks->onWindowFocusChanged = onWindowFocusChanged; + activity->callbacks->onNativeWindowCreated = onNativeWindowCreated; + activity->callbacks->onNativeWindowRedrawNeeded = onNativeWindowRedrawNeeded; + activity->callbacks->onNativeWindowDestroyed = onNativeWindowDestroyed; + activity->callbacks->onInputQueueCreated = onInputQueueCreated; + activity->callbacks->onInputQueueDestroyed = onInputQueueDestroyed; + activity->callbacks->onConfigurationChanged = onConfigurationChanged; + activity->callbacks->onLowMemory = onLowMemory; + + onCreate(activity); +} + +// TODO(crawshaw): Test configuration on more devices. +static const EGLint RGB_888[] = { + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL_SURFACE_TYPE, EGL_WINDOW_BIT, + EGL_BLUE_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_RED_SIZE, 8, + EGL_DEPTH_SIZE, 16, + EGL_CONFIG_CAVEAT, EGL_NONE, + EGL_NONE +}; + +EGLDisplay display = NULL; +EGLSurface surface = NULL; +EGLContext context = NULL; + +static char* initEGLDisplay() { + display = eglGetDisplay(EGL_DEFAULT_DISPLAY); + if (!eglInitialize(display, 0, 0)) { + return "EGL initialize failed"; + } + return NULL; +} + +char* createEGLSurface(ANativeWindow* window) { + char* err; + EGLint numConfigs, format; + EGLConfig config; + + if (display == 0) { + if ((err = initEGLDisplay()) != NULL) { + return err; + } + } + + if (!eglChooseConfig(display, RGB_888, &config, 1, &numConfigs)) { + return "EGL choose RGB_888 config failed"; + } + if (numConfigs <= 0) { + return "EGL no config found"; + } + + eglGetConfigAttrib(display, config, EGL_NATIVE_VISUAL_ID, &format); + if (ANativeWindow_setBuffersGeometry(window, 0, 0, format) != 0) { + return "EGL set buffers geometry failed"; + } + + surface = eglCreateWindowSurface(display, config, window, NULL); + if (surface == EGL_NO_SURFACE) { + return "EGL create surface failed"; + } + + if (context == NULL) { + const EGLint contextAttribs[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE }; + context = eglCreateContext(display, config, EGL_NO_CONTEXT, contextAttribs); + } + + if (eglMakeCurrent(display, surface, surface, context) == EGL_FALSE) { + return "eglMakeCurrent failed"; + } + return NULL; +} + +char* destroyEGLSurface() { + if (!eglDestroySurface(display, surface)) { + return "EGL destroy surface failed"; + } + return NULL; +} + +void finish(JNIEnv* env, jobject ctx) { + (*env)->CallVoidMethod( + env, + ctx, + finish_method); +} + +int32_t getKeyRune(JNIEnv* env, AInputEvent* e) { + return (int32_t)(*env)->CallStaticIntMethod( + env, + current_class, + key_rune_method, + AInputEvent_getDeviceId(e), + AKeyEvent_getKeyCode(e), + AKeyEvent_getMetaState(e) + ); +} + +void showKeyboard(JNIEnv* env, int keyboardType) { + (*env)->CallStaticVoidMethod( + env, + current_class, + show_keyboard_method, + keyboardType + ); +} + +void hideKeyboard(JNIEnv* env) { + (*env)->CallStaticVoidMethod( + env, + current_class, + hide_keyboard_method + ); +} + +void showFileOpen(JNIEnv* env, char* mimes) { + jstring mimesJString = (*env)->NewStringUTF(env, mimes); + (*env)->CallStaticVoidMethod( + env, + current_class, + show_file_open_method, + mimesJString + ); +} + +void showFileSave(JNIEnv* env, char* mimes, char* filename) { + jstring mimesJString = (*env)->NewStringUTF(env, mimes); + jstring filenameJString = (*env)->NewStringUTF(env, filename); + (*env)->CallStaticVoidMethod( + env, + current_class, + show_file_save_method, + mimesJString, + filenameJString + ); +} + +void Java_org_golang_app_GoNativeActivity_filePickerReturned(JNIEnv *env, jclass clazz, jstring str) { + const char* cstr = (*env)->GetStringUTFChars(env, str, JNI_FALSE); + filePickerReturned((char*)cstr); +} + +void Java_org_golang_app_GoNativeActivity_insetsChanged(JNIEnv *env, jclass clazz, int top, int bottom, int left, int right) { + insetsChanged(top, bottom, left, right); +} + +void Java_org_golang_app_GoNativeActivity_keyboardTyped(JNIEnv *env, jclass clazz, jstring str) { + const char* cstr = (*env)->GetStringUTFChars(env, str, JNI_FALSE); + keyboardTyped((char*)cstr); +} + +void Java_org_golang_app_GoNativeActivity_keyboardDelete(JNIEnv *env, jclass clazz) { + keyboardDelete(); +} + +void Java_org_golang_app_GoNativeActivity_backPressed(JNIEnv *env, jclass clazz) { + onBackPressed(); +} + +void Java_org_golang_app_GoNativeActivity_setDarkMode(JNIEnv *env, jclass clazz, jboolean dark) { + setDarkMode((bool)dark); +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/android.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/android.go new file mode 100644 index 0000000..ea925bb --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/android.go @@ -0,0 +1,898 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build android + +/* +Android Apps are built with -buildmode=c-shared. They are loaded by a +running Java process. + +Before any entry point is reached, a global constructor initializes the +Go runtime, calling all Go init functions. All cgo calls will block +until this is complete. Next JNI_OnLoad is called. When that is +complete, one of two entry points is called. + +All-Go apps built using NativeActivity enter at ANativeActivity_onCreate. + +Go libraries (for example, those built with gomobile bind) do not use +the app package initialization. +*/ + +package app + +/* +#cgo LDFLAGS: -landroid -llog -lEGL -lGLESv2 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern EGLDisplay display; +extern EGLSurface surface; + +char* createEGLSurface(ANativeWindow* window); +char* destroyEGLSurface(); +int32_t getKeyRune(JNIEnv* env, AInputEvent* e); + +void showKeyboard(JNIEnv* env, int keyboardType); +void hideKeyboard(JNIEnv* env); +void showFileOpen(JNIEnv* env, char* mimes); +void showFileSave(JNIEnv* env, char* mimes, char* filename); +void finish(JNIEnv* env, jobject ctx); + +void Java_org_golang_app_GoNativeActivity_filePickerReturned(JNIEnv *env, jclass clazz, jstring str); +*/ +import "C" + +import ( + "fmt" + "log" + "mime" + "os" + "strings" + "time" + "unsafe" + + "fyne.io/fyne/v2/internal/driver/mobile/app/callfn" + "fyne.io/fyne/v2/internal/driver/mobile/event/key" + "fyne.io/fyne/v2/internal/driver/mobile/event/lifecycle" + "fyne.io/fyne/v2/internal/driver/mobile/event/paint" + "fyne.io/fyne/v2/internal/driver/mobile/event/size" + "fyne.io/fyne/v2/internal/driver/mobile/event/touch" + "fyne.io/fyne/v2/internal/driver/mobile/mobileinit" +) + +// mimeMap contains standard mime entries that are missing on Android +var mimeMap = map[string]string{ + ".txt": "text/plain", +} + +// GoBack asks the OS to go to the previous app / activity +func GoBack() { + err := RunOnJVM(func(_, jniEnv, ctx uintptr) error { + env := (*C.JNIEnv)(unsafe.Pointer(jniEnv)) + C.finish(env, C.jobject(ctx)) + return nil + }) + if err != nil { + log.Fatalf("app: %v", err) + } +} + +// RunOnJVM runs fn on a new goroutine locked to an OS thread with a JNIEnv. +// +// RunOnJVM blocks until the call to fn is complete. Any Java +// exception or failure to attach to the JVM is returned as an error. +// +// The function fn takes vm, the current JavaVM*, +// env, the current JNIEnv*, and +// ctx, a jobject representing the global android.context.Context. +func RunOnJVM(fn func(vm, jniEnv, ctx uintptr) error) error { + return mobileinit.RunOnJVM(fn) +} + +//export setCurrentContext +func setCurrentContext(vm *C.JavaVM, ctx C.jobject) { + mobileinit.SetCurrentContext(unsafe.Pointer(vm), uintptr(ctx)) +} + +//export callMain +func callMain(mainPC uintptr) { + for _, name := range []string{"FILESDIR", "TMPDIR", "PATH", "LD_LIBRARY_PATH"} { + n := C.CString(name) + os.Setenv(name, C.GoString(C.getenv(n))) + C.free(unsafe.Pointer(n)) + } + + // Set timezone. + // + // Note that Android zoneinfo is stored in /system/usr/share/zoneinfo, + // but it is in some kind of packed TZiff file that we do not support + // yet. As a stopgap, we build a fixed zone using the tm_zone name. + var curtime C.time_t + var curtm C.struct_tm + C.time(&curtime) + C.localtime_r(&curtime, &curtm) + tzOffset := int(curtm.tm_gmtoff) + tz := C.GoString(curtm.tm_zone) + time.Local = time.FixedZone(tz, tzOffset) + + go callfn.CallFn(mainPC) +} + +//export onSaveInstanceState +func onSaveInstanceState(activity *C.ANativeActivity, outSize *C.size_t) unsafe.Pointer { + return nil +} + +//export onBackPressed +func onBackPressed() { + k := key.Event{ + Code: key.CodeBackButton, + Direction: key.DirPress, + } + theApp.events.In() <- k + + k.Direction = key.DirRelease + theApp.events.In() <- k +} + +//export onCreate +func onCreate(activity *C.ANativeActivity) { + // Set the initial configuration. + // + // Note we use unbuffered channels to talk to the activity loop, and + // NativeActivity calls these callbacks sequentially, so configuration + // will be set before <-windowRedrawNeeded is processed. + windowConfigChange <- windowConfigRead(activity) +} + +//export onDestroy +func onDestroy(activity *C.ANativeActivity) { + activityDestroyed <- struct{}{} +} + +//export onWindowFocusChanged +func onWindowFocusChanged(activity *C.ANativeActivity, hasFocus C.int) { +} + +//export onNativeWindowCreated +func onNativeWindowCreated(activity *C.ANativeActivity, window *C.ANativeWindow) { + windowCreated <- window +} + +//export onNativeWindowRedrawNeeded +func onNativeWindowRedrawNeeded(activity *C.ANativeActivity, window *C.ANativeWindow) { + // Called on orientation change and window resize. + // Send a request for redraw, and block this function + // until a complete draw and buffer swap is completed. + // This is required by the redraw documentation to + // avoid bad draws. + windowRedrawNeeded <- window + <-windowRedrawDone +} + +//export onNativeWindowDestroyed +func onNativeWindowDestroyed(activity *C.ANativeActivity, window *C.ANativeWindow) { + windowDestroyed <- window +} + +//export onInputQueueCreated +func onInputQueueCreated(activity *C.ANativeActivity, q *C.AInputQueue) { + inputQueue <- q + <-inputQueueDone +} + +//export onInputQueueDestroyed +func onInputQueueDestroyed(activity *C.ANativeActivity, q *C.AInputQueue) { + inputQueue <- nil + <-inputQueueDone +} + +//export onContentRectChanged +func onContentRectChanged(activity *C.ANativeActivity, rect *C.ARect) { +} + +//export setDarkMode +func setDarkMode(dark C.bool) { + darkMode = bool(dark) +} + +type windowConfig struct { + orientation size.Orientation + pixelsPerPt float32 +} + +func windowConfigRead(activity *C.ANativeActivity) windowConfig { + aconfig := C.AConfiguration_new() + C.AConfiguration_fromAssetManager(aconfig, activity.assetManager) + orient := C.AConfiguration_getOrientation(aconfig) + density := C.AConfiguration_getDensity(aconfig) + C.AConfiguration_delete(aconfig) + + // Calculate the screen resolution. This value is approximate. For example, + // a physical resolution of 200 DPI may be quantized to one of the + // ACONFIGURATION_DENSITY_XXX values such as 160 or 240. + // + // A more accurate DPI could possibly be calculated from + // https://developer.android.com/reference/android/util/DisplayMetrics.html#xdpi + // but this does not appear to be accessible via the NDK. In any case, the + // hardware might not even provide a more accurate number, as the system + // does not apparently use the reported value. See golang.org/issue/13366 + // for a discussion. + var dpi int + switch density { + case C.ACONFIGURATION_DENSITY_DEFAULT: + dpi = 160 + case C.ACONFIGURATION_DENSITY_LOW, + C.ACONFIGURATION_DENSITY_MEDIUM, + 213, // C.ACONFIGURATION_DENSITY_TV + C.ACONFIGURATION_DENSITY_HIGH, + 320, // ACONFIGURATION_DENSITY_XHIGH + 480, // ACONFIGURATION_DENSITY_XXHIGH + 640: // ACONFIGURATION_DENSITY_XXXHIGH + dpi = int(density) + case C.ACONFIGURATION_DENSITY_NONE: + log.Print("android device reports no screen density") + dpi = 72 + default: + log.Printf("android device reports unknown density: %d", density) + // All we can do is guess. + if density > 0 { + dpi = int(density) + } else { + dpi = 72 + } + } + + o := size.OrientationUnknown + switch orient { + case C.ACONFIGURATION_ORIENTATION_PORT: + o = size.OrientationPortrait + case C.ACONFIGURATION_ORIENTATION_LAND: + o = size.OrientationLandscape + } + + return windowConfig{ + orientation: o, + pixelsPerPt: float32(dpi) / 72, + } +} + +//export onConfigurationChanged +func onConfigurationChanged(activity *C.ANativeActivity) { + // A rotation event first triggers onConfigurationChanged, then + // calls onNativeWindowRedrawNeeded. We extract the orientation + // here and save it for the redraw event. + windowConfigChange <- windowConfigRead(activity) +} + +//export onLowMemory +func onLowMemory(activity *C.ANativeActivity) { + cleanCaches() +} + +var ( + inputQueue = make(chan *C.AInputQueue) + inputQueueDone = make(chan struct{}) + windowCreated = make(chan *C.ANativeWindow) + windowDestroyed = make(chan *C.ANativeWindow) + windowRedrawNeeded = make(chan *C.ANativeWindow) + windowRedrawDone = make(chan struct{}) + windowConfigChange = make(chan windowConfig) + activityDestroyed = make(chan struct{}) + + currentSize size.Event + darkMode bool +) + +func init() { + theApp.registerGLViewportFilter() +} + +func main(f func(App)) { + mainUserFn = f + // TODO: merge the runInputQueue and mainUI functions? + go func() { + if err := mobileinit.RunOnJVM(runInputQueue); err != nil { + log.Fatalf("app: %v", err) + } + }() + // Preserve this OS thread for: + // 1. the attached JNI thread + // 2. the GL context + if err := mobileinit.RunOnJVM(mainUI); err != nil { + log.Fatalf("app: %v", err) + } +} + +// driverShowVirtualKeyboard requests the driver to show a virtual keyboard for text input +func driverShowVirtualKeyboard(keyboard KeyboardType) { + err := mobileinit.RunOnJVM(func(vm, jniEnv, ctx uintptr) error { + env := (*C.JNIEnv)(unsafe.Pointer(jniEnv)) // not a Go heap pointer + C.showKeyboard(env, C.int(int32(keyboard))) + return nil + }) + if err != nil { + log.Fatalf("app: %v", err) + } +} + +// driverHideVirtualKeyboard requests the driver to hide any visible virtual keyboard +func driverHideVirtualKeyboard() { + if err := mobileinit.RunOnJVM(hideSoftInput); err != nil { + log.Fatalf("app: %v", err) + } +} + +func hideSoftInput(vm, jniEnv, ctx uintptr) error { + env := (*C.JNIEnv)(unsafe.Pointer(jniEnv)) // not a Go heap pointer + C.hideKeyboard(env) + return nil +} + +var fileCallback func(string, func()) + +//export filePickerReturned +func filePickerReturned(str *C.char) { + if fileCallback == nil { + return + } + + fileCallback(C.GoString(str), nil) + fileCallback = nil +} + +//export insetsChanged +func insetsChanged(top, bottom, left, right int) { + currentSize.InsetTopPx = top + currentSize.InsetBottomPx = bottom + currentSize.InsetLeftPx = left + currentSize.InsetRightPx = right + + theApp.events.In() <- currentSize +} + +func mimeStringFromFilter(filter *FileFilter) string { + mimes := "*/*" + if filter.MimeTypes != nil { + mimes = strings.Join(filter.MimeTypes, "|") + } else if filter.Extensions != nil { + var mimeTypes []string + for _, ext := range filter.Extensions { + if mimeEntry, ok := mimeMap[ext]; ok { + mimeTypes = append(mimeTypes, mimeEntry) + + continue + } + + mimeType := mime.TypeByExtension(ext) + if mimeType == "" { + log.Println("Could not find mime for extension " + ext + ", allowing all") + return "*/*" // could not find one, so allow all + } + + mimeTypes = append(mimeTypes, mimeType) + } + mimes = strings.Join(mimeTypes, "|") + } + return mimes +} + +func driverShowFileOpenPicker(callback func(string, func()), filter *FileFilter) { + fileCallback = callback + + mimes := mimeStringFromFilter(filter) + mimeStr := C.CString(mimes) + defer C.free(unsafe.Pointer(mimeStr)) + + open := func(vm, jniEnv, ctx uintptr) error { + // TODO pass in filter... + env := (*C.JNIEnv)(unsafe.Pointer(jniEnv)) // not a Go heap pointer + C.showFileOpen(env, mimeStr) + return nil + } + + if err := mobileinit.RunOnJVM(open); err != nil { + log.Fatalf("app: %v", err) + } +} + +func driverShowFileSavePicker(callback func(string, func()), filter *FileFilter, filename string) { + fileCallback = callback + + mimes := mimeStringFromFilter(filter) + mimeStr := C.CString(mimes) + defer C.free(unsafe.Pointer(mimeStr)) + filenameStr := C.CString(filename) + defer C.free(unsafe.Pointer(filenameStr)) + + save := func(vm, jniEnv, ctx uintptr) error { + env := (*C.JNIEnv)(unsafe.Pointer(jniEnv)) // not a Go heap pointer + C.showFileSave(env, mimeStr, filenameStr) + return nil + } + + if err := mobileinit.RunOnJVM(save); err != nil { + log.Fatalf("app: %v", err) + } +} + +var mainUserFn func(App) + +var DisplayMetrics struct { + WidthPx int + HeightPx int +} + +func mainUI(vm, jniEnv, ctx uintptr) error { + workAvailable := theApp.worker.WorkAvailable() + + donec := make(chan struct{}) + go func() { + mainUserFn(theApp) + close(donec) + }() + + var pixelsPerPt float32 + var surfaceInitialized, wasDestroyed bool + + for { + select { + case <-donec: + return nil + case cfg := <-windowConfigChange: + pixelsPerPt = cfg.pixelsPerPt + case w := <-windowCreated: + if surfaceInitialized && !wasDestroyed { + if errStr := C.destroyEGLSurface(); errStr != nil { + return fmt.Errorf("%s (%s)", C.GoString(errStr), eglGetError()) + } + if errStr := C.createEGLSurface(w); errStr != nil { + return fmt.Errorf("%s (%s)", C.GoString(errStr), eglGetError()) + } + } + case w := <-windowRedrawNeeded: + if C.surface == nil { + if errStr := C.createEGLSurface(w); errStr != nil { + return fmt.Errorf("%s (%s)", C.GoString(errStr), eglGetError()) + } + surfaceInitialized = true + DisplayMetrics.WidthPx = int(C.ANativeWindow_getWidth(w)) + DisplayMetrics.HeightPx = int(C.ANativeWindow_getHeight(w)) + } + theApp.sendLifecycle(lifecycle.StageFocused) + widthPx := int(C.ANativeWindow_getWidth(w)) + heightPx := int(C.ANativeWindow_getHeight(w)) + currentSize = size.Event{ + WidthPx: widthPx, + HeightPx: heightPx, + WidthPt: float32(widthPx) / pixelsPerPt, + HeightPt: float32(heightPx) / pixelsPerPt, + InsetTopPx: currentSize.InsetTopPx, + InsetBottomPx: currentSize.InsetBottomPx, + InsetLeftPx: currentSize.InsetLeftPx, + InsetRightPx: currentSize.InsetRightPx, + PixelsPerPt: pixelsPerPt, + Orientation: screenOrientation(widthPx, heightPx), // we are guessing orientation here as it was not always working + DarkMode: darkMode, + } + theApp.events.In() <- currentSize + theApp.events.In() <- paint.Event{External: true, Window: uintptr(unsafe.Pointer(w))} + case <-windowDestroyed: + if C.surface != nil { + if errStr := C.destroyEGLSurface(); errStr != nil { + return fmt.Errorf("%s (%s)", C.GoString(errStr), eglGetError()) + } + } + C.surface = nil + wasDestroyed = true + theApp.sendLifecycle(lifecycle.StageAlive) + case <-activityDestroyed: + theApp.sendLifecycle(lifecycle.StageDead) + case <-workAvailable: + theApp.worker.DoWork() + case <-theApp.publish: + // TODO: compare a generation number to redrawGen for stale paints? + if C.surface != nil { + // eglSwapBuffers blocks until vsync. + if C.eglSwapBuffers(C.display, C.surface) == C.EGL_FALSE { + log.Printf("app: failed to swap buffers (%s)", eglGetError()) + } + } + select { + case windowRedrawDone <- struct{}{}: + default: + } + theApp.publishResult <- PublishResult{} + } + } +} + +func runInputQueue(vm, jniEnv, ctx uintptr) error { + env := (*C.JNIEnv)(unsafe.Pointer(jniEnv)) // not a Go heap pointer + + // Android loopers select on OS file descriptors, not Go channels, so we + // translate the inputQueue channel to an ALooper_wake call. + l := C.ALooper_prepare(C.ALOOPER_PREPARE_ALLOW_NON_CALLBACKS) + pending := make(chan *C.AInputQueue, 1) + go func() { + for q := range inputQueue { + pending <- q + C.ALooper_wake(l) + } + }() + + var q *C.AInputQueue + for { + if C.ALooper_pollOnce(-1, nil, nil, nil) == C.ALOOPER_POLL_WAKE { + select { + default: + case p := <-pending: + if q != nil { + processEvents(env, q) + C.AInputQueue_detachLooper(q) + } + q = p + if q != nil { + C.AInputQueue_attachLooper(q, l, 0, nil, nil) + } + inputQueueDone <- struct{}{} + } + } + if q != nil { + processEvents(env, q) + } + } +} + +func processEvents(env *C.JNIEnv, q *C.AInputQueue) { + var e *C.AInputEvent + for C.AInputQueue_getEvent(q, &e) >= 0 { + if C.AInputQueue_preDispatchEvent(q, e) != 0 { + continue + } + processEvent(env, e) + C.AInputQueue_finishEvent(q, e, 0) + } +} + +func processEvent(env *C.JNIEnv, e *C.AInputEvent) { + switch C.AInputEvent_getType(e) { + case C.AINPUT_EVENT_TYPE_KEY: + processKey(env, e) + case C.AINPUT_EVENT_TYPE_MOTION: + // At most one of the events in this batch is an up or down event; get its index and change. + upDownIndex := C.size_t(C.AMotionEvent_getAction(e)&C.AMOTION_EVENT_ACTION_POINTER_INDEX_MASK) >> C.AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT + upDownType := touch.TypeMove + switch C.AMotionEvent_getAction(e) & C.AMOTION_EVENT_ACTION_MASK { + case C.AMOTION_EVENT_ACTION_DOWN, C.AMOTION_EVENT_ACTION_POINTER_DOWN: + upDownType = touch.TypeBegin + case C.AMOTION_EVENT_ACTION_UP, C.AMOTION_EVENT_ACTION_POINTER_UP: + upDownType = touch.TypeEnd + } + + for i, n := C.size_t(0), C.AMotionEvent_getPointerCount(e); i < n; i++ { + t := touch.TypeMove + if i == upDownIndex { + t = upDownType + } + theApp.events.In() <- touch.Event{ + X: float32(C.AMotionEvent_getX(e, i)), + Y: float32(C.AMotionEvent_getY(e, i)), + Sequence: touch.Sequence(C.AMotionEvent_getPointerId(e, i)), + Type: t, + } + } + default: + log.Printf("unknown input event, type=%d", C.AInputEvent_getType(e)) + } +} + +func processKey(env *C.JNIEnv, e *C.AInputEvent) { + deviceID := C.AInputEvent_getDeviceId(e) + if deviceID == 0 { + // Software keyboard input, leaving for scribe/IME. + return + } + + k := key.Event{ + Rune: rune(C.getKeyRune(env, e)), + Code: convAndroidKeyCode(int32(C.AKeyEvent_getKeyCode(e))), + } + if k.Rune >= '0' && k.Rune <= '9' { // GBoard generates key events for numbers, but we see them in textChanged + return + } + switch C.AKeyEvent_getAction(e) { + case C.AKEY_STATE_DOWN: + k.Direction = key.DirPress + case C.AKEY_STATE_UP: + k.Direction = key.DirRelease + default: + k.Direction = key.DirNone + } + // TODO(crawshaw): set Modifiers. + theApp.events.In() <- k +} + +func eglGetError() string { + switch errNum := C.eglGetError(); errNum { + case C.EGL_SUCCESS: + return "EGL_SUCCESS" + case C.EGL_NOT_INITIALIZED: + return "EGL_NOT_INITIALIZED" + case C.EGL_BAD_ACCESS: + return "EGL_BAD_ACCESS" + case C.EGL_BAD_ALLOC: + return "EGL_BAD_ALLOC" + case C.EGL_BAD_ATTRIBUTE: + return "EGL_BAD_ATTRIBUTE" + case C.EGL_BAD_CONTEXT: + return "EGL_BAD_CONTEXT" + case C.EGL_BAD_CONFIG: + return "EGL_BAD_CONFIG" + case C.EGL_BAD_CURRENT_SURFACE: + return "EGL_BAD_CURRENT_SURFACE" + case C.EGL_BAD_DISPLAY: + return "EGL_BAD_DISPLAY" + case C.EGL_BAD_SURFACE: + return "EGL_BAD_SURFACE" + case C.EGL_BAD_MATCH: + return "EGL_BAD_MATCH" + case C.EGL_BAD_PARAMETER: + return "EGL_BAD_PARAMETER" + case C.EGL_BAD_NATIVE_PIXMAP: + return "EGL_BAD_NATIVE_PIXMAP" + case C.EGL_BAD_NATIVE_WINDOW: + return "EGL_BAD_NATIVE_WINDOW" + case C.EGL_CONTEXT_LOST: + return "EGL_CONTEXT_LOST" + default: + return fmt.Sprintf("Unknown EGL err: %d", errNum) + } +} + +var androidKeycoe = map[int32]key.Code{ + C.AKEYCODE_HOME: key.CodeHome, + C.AKEYCODE_0: key.Code0, + C.AKEYCODE_1: key.Code1, + C.AKEYCODE_2: key.Code2, + C.AKEYCODE_3: key.Code3, + C.AKEYCODE_4: key.Code4, + C.AKEYCODE_5: key.Code5, + C.AKEYCODE_6: key.Code6, + C.AKEYCODE_7: key.Code7, + C.AKEYCODE_8: key.Code8, + C.AKEYCODE_9: key.Code9, + C.AKEYCODE_VOLUME_UP: key.CodeVolumeUp, + C.AKEYCODE_VOLUME_DOWN: key.CodeVolumeDown, + C.AKEYCODE_A: key.CodeA, + C.AKEYCODE_B: key.CodeB, + C.AKEYCODE_C: key.CodeC, + C.AKEYCODE_D: key.CodeD, + C.AKEYCODE_E: key.CodeE, + C.AKEYCODE_F: key.CodeF, + C.AKEYCODE_G: key.CodeG, + C.AKEYCODE_H: key.CodeH, + C.AKEYCODE_I: key.CodeI, + C.AKEYCODE_J: key.CodeJ, + C.AKEYCODE_K: key.CodeK, + C.AKEYCODE_L: key.CodeL, + C.AKEYCODE_M: key.CodeM, + C.AKEYCODE_N: key.CodeN, + C.AKEYCODE_O: key.CodeO, + C.AKEYCODE_P: key.CodeP, + C.AKEYCODE_Q: key.CodeQ, + C.AKEYCODE_R: key.CodeR, + C.AKEYCODE_S: key.CodeS, + C.AKEYCODE_T: key.CodeT, + C.AKEYCODE_U: key.CodeU, + C.AKEYCODE_V: key.CodeV, + C.AKEYCODE_W: key.CodeW, + C.AKEYCODE_X: key.CodeX, + C.AKEYCODE_Y: key.CodeY, + C.AKEYCODE_Z: key.CodeZ, + C.AKEYCODE_COMMA: key.CodeComma, + C.AKEYCODE_PERIOD: key.CodeFullStop, + C.AKEYCODE_ALT_LEFT: key.CodeLeftAlt, + C.AKEYCODE_ALT_RIGHT: key.CodeRightAlt, + C.AKEYCODE_SHIFT_LEFT: key.CodeLeftShift, + C.AKEYCODE_SHIFT_RIGHT: key.CodeRightShift, + C.AKEYCODE_TAB: key.CodeTab, + C.AKEYCODE_SPACE: key.CodeSpacebar, + C.AKEYCODE_ENTER: key.CodeReturnEnter, + C.AKEYCODE_DEL: key.CodeDeleteBackspace, + C.AKEYCODE_GRAVE: key.CodeGraveAccent, + C.AKEYCODE_MINUS: key.CodeHyphenMinus, + C.AKEYCODE_EQUALS: key.CodeEqualSign, + C.AKEYCODE_LEFT_BRACKET: key.CodeLeftSquareBracket, + C.AKEYCODE_RIGHT_BRACKET: key.CodeRightSquareBracket, + C.AKEYCODE_BACKSLASH: key.CodeBackslash, + C.AKEYCODE_SEMICOLON: key.CodeSemicolon, + C.AKEYCODE_APOSTROPHE: key.CodeApostrophe, + C.AKEYCODE_SLASH: key.CodeSlash, + C.AKEYCODE_PAGE_UP: key.CodePageUp, + C.AKEYCODE_PAGE_DOWN: key.CodePageDown, + C.AKEYCODE_ESCAPE: key.CodeEscape, + C.AKEYCODE_FORWARD_DEL: key.CodeDeleteForward, + C.AKEYCODE_CTRL_LEFT: key.CodeLeftControl, + C.AKEYCODE_CTRL_RIGHT: key.CodeRightControl, + C.AKEYCODE_CAPS_LOCK: key.CodeCapsLock, + C.AKEYCODE_META_LEFT: key.CodeLeftGUI, + C.AKEYCODE_META_RIGHT: key.CodeRightGUI, + C.AKEYCODE_INSERT: key.CodeInsert, + C.AKEYCODE_F1: key.CodeF1, + C.AKEYCODE_F2: key.CodeF2, + C.AKEYCODE_F3: key.CodeF3, + C.AKEYCODE_F4: key.CodeF4, + C.AKEYCODE_F5: key.CodeF5, + C.AKEYCODE_F6: key.CodeF6, + C.AKEYCODE_F7: key.CodeF7, + C.AKEYCODE_F8: key.CodeF8, + C.AKEYCODE_F9: key.CodeF9, + C.AKEYCODE_F10: key.CodeF10, + C.AKEYCODE_F11: key.CodeF11, + C.AKEYCODE_F12: key.CodeF12, + C.AKEYCODE_NUM_LOCK: key.CodeKeypadNumLock, + C.AKEYCODE_NUMPAD_0: key.CodeKeypad0, + C.AKEYCODE_NUMPAD_1: key.CodeKeypad1, + C.AKEYCODE_NUMPAD_2: key.CodeKeypad2, + C.AKEYCODE_NUMPAD_3: key.CodeKeypad3, + C.AKEYCODE_NUMPAD_4: key.CodeKeypad4, + C.AKEYCODE_NUMPAD_5: key.CodeKeypad5, + C.AKEYCODE_NUMPAD_6: key.CodeKeypad6, + C.AKEYCODE_NUMPAD_7: key.CodeKeypad7, + C.AKEYCODE_NUMPAD_8: key.CodeKeypad8, + C.AKEYCODE_NUMPAD_9: key.CodeKeypad9, + C.AKEYCODE_NUMPAD_DIVIDE: key.CodeKeypadSlash, + C.AKEYCODE_NUMPAD_MULTIPLY: key.CodeKeypadAsterisk, + C.AKEYCODE_NUMPAD_SUBTRACT: key.CodeKeypadHyphenMinus, + C.AKEYCODE_NUMPAD_ADD: key.CodeKeypadPlusSign, + C.AKEYCODE_NUMPAD_DOT: key.CodeKeypadFullStop, + C.AKEYCODE_NUMPAD_ENTER: key.CodeKeypadEnter, + C.AKEYCODE_NUMPAD_EQUALS: key.CodeKeypadEqualSign, + C.AKEYCODE_VOLUME_MUTE: key.CodeMute, +} + +func convAndroidKeyCode(aKeyCode int32) key.Code { + if code, ok := androidKeycoe[aKeyCode]; ok { + return code + } + return key.CodeUnknown +} + +/* + Many Android key codes do not map into USB HID codes. + For those, key.CodeUnknown is returned. This switch has all + cases, even the unknown ones, to serve as a documentation + and search aid. + C.AKEYCODE_UNKNOWN + C.AKEYCODE_SOFT_LEFT + C.AKEYCODE_SOFT_RIGHT + C.AKEYCODE_BACK + C.AKEYCODE_CALL + C.AKEYCODE_ENDCALL + C.AKEYCODE_STAR + C.AKEYCODE_POUND + C.AKEYCODE_DPAD_UP + C.AKEYCODE_DPAD_DOWN + C.AKEYCODE_DPAD_LEFT + C.AKEYCODE_DPAD_RIGHT + C.AKEYCODE_DPAD_CENTER + C.AKEYCODE_POWER + C.AKEYCODE_CAMERA + C.AKEYCODE_CLEAR + C.AKEYCODE_SYM + C.AKEYCODE_EXPLORER + C.AKEYCODE_ENVELOPE + C.AKEYCODE_AT + C.AKEYCODE_NUM + C.AKEYCODE_HEADSETHOOK + C.AKEYCODE_FOCUS + C.AKEYCODE_PLUS + C.AKEYCODE_MENU + C.AKEYCODE_NOTIFICATION + C.AKEYCODE_SEARCH + C.AKEYCODE_MEDIA_PLAY_PAUSE + C.AKEYCODE_MEDIA_STOP + C.AKEYCODE_MEDIA_NEXT + C.AKEYCODE_MEDIA_PREVIOUS + C.AKEYCODE_MEDIA_REWIND + C.AKEYCODE_MEDIA_FAST_FORWARD + C.AKEYCODE_MUTE + C.AKEYCODE_PICTSYMBOLS + C.AKEYCODE_SWITCH_CHARSET + C.AKEYCODE_BUTTON_A + C.AKEYCODE_BUTTON_B + C.AKEYCODE_BUTTON_C + C.AKEYCODE_BUTTON_X + C.AKEYCODE_BUTTON_Y + C.AKEYCODE_BUTTON_Z + C.AKEYCODE_BUTTON_L1 + C.AKEYCODE_BUTTON_R1 + C.AKEYCODE_BUTTON_L2 + C.AKEYCODE_BUTTON_R2 + C.AKEYCODE_BUTTON_THUMBL + C.AKEYCODE_BUTTON_THUMBR + C.AKEYCODE_BUTTON_START + C.AKEYCODE_BUTTON_SELECT + C.AKEYCODE_BUTTON_MODE + C.AKEYCODE_SCROLL_LOCK + C.AKEYCODE_FUNCTION + C.AKEYCODE_SYSRQ + C.AKEYCODE_BREAK + C.AKEYCODE_MOVE_HOME + C.AKEYCODE_MOVE_END + C.AKEYCODE_FORWARD + C.AKEYCODE_MEDIA_PLAY + C.AKEYCODE_MEDIA_PAUSE + C.AKEYCODE_MEDIA_CLOSE + C.AKEYCODE_MEDIA_EJECT + C.AKEYCODE_MEDIA_RECORD + C.AKEYCODE_NUMPAD_COMMA + C.AKEYCODE_NUMPAD_LEFT_PAREN + C.AKEYCODE_NUMPAD_RIGHT_PAREN + C.AKEYCODE_INFO + C.AKEYCODE_CHANNEL_UP + C.AKEYCODE_CHANNEL_DOWN + C.AKEYCODE_ZOOM_IN + C.AKEYCODE_ZOOM_OUT + C.AKEYCODE_TV + C.AKEYCODE_WINDOW + C.AKEYCODE_GUIDE + C.AKEYCODE_DVR + C.AKEYCODE_BOOKMARK + C.AKEYCODE_CAPTIONS + C.AKEYCODE_SETTINGS + C.AKEYCODE_TV_POWER + C.AKEYCODE_TV_INPUT + C.AKEYCODE_STB_POWER + C.AKEYCODE_STB_INPUT + C.AKEYCODE_AVR_POWER + C.AKEYCODE_AVR_INPUT + C.AKEYCODE_PROG_RED + C.AKEYCODE_PROG_GREEN + C.AKEYCODE_PROG_YELLOW + C.AKEYCODE_PROG_BLUE + C.AKEYCODE_APP_SWITCH + C.AKEYCODE_BUTTON_1 + C.AKEYCODE_BUTTON_2 + C.AKEYCODE_BUTTON_3 + C.AKEYCODE_BUTTON_4 + C.AKEYCODE_BUTTON_5 + C.AKEYCODE_BUTTON_6 + C.AKEYCODE_BUTTON_7 + C.AKEYCODE_BUTTON_8 + C.AKEYCODE_BUTTON_9 + C.AKEYCODE_BUTTON_10 + C.AKEYCODE_BUTTON_11 + C.AKEYCODE_BUTTON_12 + C.AKEYCODE_BUTTON_13 + C.AKEYCODE_BUTTON_14 + C.AKEYCODE_BUTTON_15 + C.AKEYCODE_BUTTON_16 + C.AKEYCODE_LANGUAGE_SWITCH + C.AKEYCODE_MANNER_MODE + C.AKEYCODE_3D_MODE + C.AKEYCODE_CONTACTS + C.AKEYCODE_CALENDAR + C.AKEYCODE_MUSIC + C.AKEYCODE_CALCULATOR + + Defined in an NDK API version beyond what we use today: + C.AKEYCODE_ASSIST + C.AKEYCODE_BRIGHTNESS_DOWN + C.AKEYCODE_BRIGHTNESS_UP + C.AKEYCODE_RO + C.AKEYCODE_YEN + C.AKEYCODE_ZENKAKU_HANKAKU +*/ diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/app.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/app.go new file mode 100644 index 0000000..d0aaa8e --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/app.go @@ -0,0 +1,166 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build freebsd || linux || darwin || windows || openbsd + +package app + +import ( + "fyne.io/fyne/v2/internal/async" + "fyne.io/fyne/v2/internal/driver/mobile/event/lifecycle" + "fyne.io/fyne/v2/internal/driver/mobile/event/size" + "fyne.io/fyne/v2/internal/driver/mobile/gl" + + // Initialize necessary mobile functionality, such as logging. + _ "fyne.io/fyne/v2/internal/driver/mobile/mobileinit" +) + +// Main is called by the main.main function to run the mobile application. +// +// It calls f on the App, in a separate goroutine, as some OS-specific +// libraries require being on 'the main thread'. +func Main(f func(App)) { + main(f) +} + +// App is how a GUI mobile application interacts with the OS. +type App interface { + // Events returns the events channel. It carries events from the system to + // the app. The type of such events include: + // - lifecycle.Event + // - mouse.Event + // - paint.Event + // - size.Event + // - touch.Event + // from the golang.org/x/mobile/event/etc packages. Other packages may + // define other event types that are carried on this channel. + Events() <-chan any + + // Send sends an event on the events channel. It does not block. + Send(event any) + + // Publish flushes any pending drawing commands, such as OpenGL calls, and + // swaps the back buffer to the screen. + Publish() PublishResult + + // TODO: replace filters (and the Events channel) with a NextEvent method? + + // Filter calls each registered event filter function in sequence. + Filter(event any) any + + // RegisterFilter registers a event filter function to be called by Filter. The + // function can return a different event, or return nil to consume the event, + // but the function can also return its argument unchanged, where its purpose + // is to trigger a side effect rather than modify the event. + RegisterFilter(f func(any) any) + + ShowVirtualKeyboard(KeyboardType) + HideVirtualKeyboard() + ShowFileOpenPicker(func(string, func()), *FileFilter) + ShowFileSavePicker(func(string, func()), *FileFilter, string) +} + +// FileFilter is a filter of files. +type FileFilter struct { + Extensions []string + MimeTypes []string +} + +// PublishResult is the result of an App.Publish call. +type PublishResult struct { + // BackBufferPreserved is whether the contents of the back buffer was + // preserved. If false, the contents are undefined. + BackBufferPreserved bool +} + +var theApp = &app{ + events: async.NewUnboundedChan[any](), + lifecycleStage: lifecycle.StageDead, + publish: make(chan struct{}), + publishResult: make(chan PublishResult), +} + +func init() { + theApp.glctx, theApp.worker = gl.NewContext() +} + +func (a *app) sendLifecycle(to lifecycle.Stage) { + if a.lifecycleStage == to { + return + } + a.events.In() <- lifecycle.Event{ + From: a.lifecycleStage, + To: to, + DrawContext: a.glctx, + } + a.lifecycleStage = to +} + +type app struct { + filters []func(any) any + + events *async.UnboundedChan[any] + lifecycleStage lifecycle.Stage + publish chan struct{} + publishResult chan PublishResult + + glctx gl.Context + worker gl.Worker +} + +func (a *app) Events() <-chan any { + return a.events.Out() +} + +func (a *app) Send(event any) { + a.events.In() <- event +} + +func (a *app) Publish() PublishResult { + // gl.Flush is a lightweight (on modern GL drivers) blocking call + // that ensures all GL functions pending in the gl package have + // been passed onto the GL driver before the app package attempts + // to swap the screen buffer. + // + // This enforces that the final receive (for this paint cycle) on + // gl.WorkAvailable happens before the send on endPaint. + a.glctx.Flush() + a.publish <- struct{}{} + return <-a.publishResult +} + +func (a *app) Filter(event any) any { + for _, f := range a.filters { + event = f(event) + } + return event +} + +func (a *app) RegisterFilter(f func(any) any) { + a.filters = append(a.filters, f) +} + +func (a *app) ShowVirtualKeyboard(keyboard KeyboardType) { + driverShowVirtualKeyboard(keyboard) +} + +func (a *app) HideVirtualKeyboard() { + driverHideVirtualKeyboard() +} + +func (a *app) ShowFileOpenPicker(callback func(string, func()), filter *FileFilter) { + driverShowFileOpenPicker(callback, filter) +} + +func (a *app) ShowFileSavePicker(callback func(string, func()), filter *FileFilter, filename string) { + driverShowFileSavePicker(callback, filter, filename) +} + +func screenOrientation(width, height int) size.Orientation { + if width > height { + return size.OrientationLandscape + } + + return size.OrientationPortrait +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/app_unix.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/app_unix.go new file mode 100644 index 0000000..e7e72b9 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/app_unix.go @@ -0,0 +1,19 @@ +//go:build freebsd || linux || openbsd + +package app + +import "fyne.io/fyne/v2/internal/driver/mobile/event/size" + +// TODO: do this for all build targets, not just linux (x11 and Android)? If +// so, should package gl instead of this package call RegisterFilter?? +// +// TODO: does Android need this?? It seems to work without it (Nexus 7, +// KitKat). If only x11 needs this, should we move this to x11.go?? +func (a *app) registerGLViewportFilter() { + a.RegisterFilter(func(e any) any { + if e, ok := e.(size.Event); ok { + a.glctx.Viewport(0, 0, e.WidthPx, e.HeightPx) + } + return e + }) +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/callfn/callfn.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/callfn/callfn.go new file mode 100644 index 0000000..ecc3d45 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/callfn/callfn.go @@ -0,0 +1,16 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build android && (arm || 386 || amd64 || arm64) + +// Package callfn provides an android entry point. +// +// It is a separate package from app because it contains Go assembly, +// which does not compile in a package using cgo. +package callfn + +// CallFn calls a zero-argument function by its program counter. +// It is only intended for calling main.main. Using it for +// anything else will not end well. +func CallFn(fn uintptr) diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/callfn/callfn_386.s b/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/callfn/callfn_386.s new file mode 100644 index 0000000..d2bb54f --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/callfn/callfn_386.s @@ -0,0 +1,11 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "textflag.h" +#include "funcdata.h" + +TEXT ·CallFn(SB),$0-4 + MOVL fn+0(FP), AX + CALL AX + RET diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/callfn/callfn_amd64.s b/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/callfn/callfn_amd64.s new file mode 100644 index 0000000..8769604 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/callfn/callfn_amd64.s @@ -0,0 +1,11 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "textflag.h" +#include "funcdata.h" + +TEXT ·CallFn(SB),$0-8 + MOVQ fn+0(FP), AX + CALL AX + RET diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/callfn/callfn_arm.s b/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/callfn/callfn_arm.s new file mode 100644 index 0000000..d71f748 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/callfn/callfn_arm.s @@ -0,0 +1,11 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "textflag.h" +#include "funcdata.h" + +TEXT ·CallFn(SB),$0-4 + MOVW fn+0(FP), R0 + BL (R0) + RET diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/callfn/callfn_arm64.s b/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/callfn/callfn_arm64.s new file mode 100644 index 0000000..545ad50 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/callfn/callfn_arm64.s @@ -0,0 +1,11 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "textflag.h" +#include "funcdata.h" + +TEXT ·CallFn(SB),$0-8 + MOVD fn+0(FP), R0 + BL (R0) + RET diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/darwin_desktop.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/darwin_desktop.go new file mode 100644 index 0000000..0ca9b85 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/darwin_desktop.go @@ -0,0 +1,414 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build darwin && !ios + +package app + +// Simple on-screen app debugging for OS X. Not an officially supported +// development target for apps, as screens with mice are very different +// than screens with touch panels. + +/* +#cgo CFLAGS: -x objective-c -DGL_SILENCE_DEPRECATION +#cgo LDFLAGS: -framework Cocoa -framework OpenGL +#import // for HIToolbox/Events.h +#import +#include + +void runApp(void); +void stopApp(void); +void makeCurrentContext(GLintptr); +uint64 threadID(); +*/ +import "C" + +import ( + "log" + "runtime" + + "fyne.io/fyne/v2/internal/driver/mobile/event/key" + "fyne.io/fyne/v2/internal/driver/mobile/event/lifecycle" + "fyne.io/fyne/v2/internal/driver/mobile/event/paint" + "fyne.io/fyne/v2/internal/driver/mobile/event/size" + "fyne.io/fyne/v2/internal/driver/mobile/event/touch" +) + +var initThreadID uint64 + +func init() { + // Lock the goroutine responsible for initialization to an OS thread. + // This means the goroutine running main (and calling runApp below) + // is locked to the OS thread that started the program. This is + // necessary for the correct delivery of Cocoa events to the process. + // + // A discussion on this topic: + // https://groups.google.com/forum/#!msg/golang-nuts/IiWZ2hUuLDA/SNKYYZBelsYJ + runtime.LockOSThread() + initThreadID = uint64(C.threadID()) +} + +func main(f func(App)) { + if tid := uint64(C.threadID()); tid != initThreadID { + log.Fatalf("app.Main called on thread %d, but app.init ran on %d", tid, initThreadID) + } + + go func() { + f(theApp) + C.stopApp() + // TODO(crawshaw): trigger runApp to return + }() + + C.runApp() +} + +func GoBack() { + // When simulating mobile there are no other activities open (and we can't just force background) +} + +// loop is the primary drawing loop. +// +// After Cocoa has captured the initial OS thread for processing Cocoa +// events in runApp, it starts loop on another goroutine. It is locked +// to an OS thread for its OpenGL context. +// +// The loop processes GL calls until a publish event appears. +// Then it runs any remaining GL calls and flushes the screen. +// +// As NSOpenGLCPSwapInterval is set to 1, the call to CGLFlushDrawable +// blocks until the screen refresh. +func (a *app) loop(ctx C.GLintptr) { + runtime.LockOSThread() + C.makeCurrentContext(ctx) + + workAvailable := a.worker.WorkAvailable() + + for { + select { + case <-workAvailable: + a.worker.DoWork() + case <-theApp.publish: + loop1: + for { + select { + case <-workAvailable: + a.worker.DoWork() + default: + break loop1 + } + } + C.CGLFlushDrawable(C.CGLGetCurrentContext()) + theApp.publishResult <- PublishResult{} + select { + case drawDone <- struct{}{}: + default: + } + } + } +} + +var drawDone = make(chan struct{}) + +// drawgl is used by Cocoa to occasionally request screen updates. +// +//export drawgl +func drawgl() { + switch theApp.lifecycleStage { + case lifecycle.StageFocused, lifecycle.StageVisible: + theApp.Send(paint.Event{ + External: true, + }) + <-drawDone + } +} + +//export startloop +func startloop(ctx C.GLintptr) { + go theApp.loop(ctx) +} + +var windowHeightPx float32 + +//export setGeom +func setGeom(pixelsPerPt float32, widthPx, heightPx int) { + windowHeightPx = float32(heightPx) + theApp.events.In() <- size.Event{ + WidthPx: widthPx, + HeightPx: heightPx, + WidthPt: float32(widthPx) / pixelsPerPt, + HeightPt: float32(heightPx) / pixelsPerPt, + PixelsPerPt: pixelsPerPt, + Orientation: screenOrientation(widthPx, heightPx), + } +} + +func sendTouch(t touch.Type, x, y float32) { + theApp.events.In() <- touch.Event{ + X: x, + Y: windowHeightPx - y, + Sequence: 0, + Type: t, + } +} + +//export eventMouseDown +func eventMouseDown(x, y float32) { sendTouch(touch.TypeBegin, x, y) } + +//export eventMouseDragged +func eventMouseDragged(x, y float32) { sendTouch(touch.TypeMove, x, y) } + +//export eventMouseEnd +func eventMouseEnd(x, y float32) { sendTouch(touch.TypeEnd, x, y) } + +var stopped = false + +//export lifecycleDead +func lifecycleDead() { + if stopped { + return + } + stopped = true + + theApp.sendLifecycle(lifecycle.StageDead) + theApp.events.Close() +} + +//export eventKey +func eventKey(runeVal int32, direction uint8, code uint16, flags uint32) { + var modifiers key.Modifiers + for _, mod := range mods { + if flags&mod.flags == mod.flags { + modifiers |= mod.mod + } + } + + theApp.events.In() <- key.Event{ + Rune: convRune(rune(runeVal)), + Code: convVirtualKeyCode(code), + Modifiers: modifiers, + Direction: key.Direction(direction), + } +} + +//export eventFlags +func eventFlags(flags uint32) { + for _, mod := range mods { + if flags&mod.flags == mod.flags && lastFlags&mod.flags != mod.flags { + eventKey(-1, uint8(key.DirPress), mod.code, flags) + } + if lastFlags&mod.flags == mod.flags && flags&mod.flags != mod.flags { + eventKey(-1, uint8(key.DirRelease), mod.code, flags) + } + } + lastFlags = flags +} + +var lastFlags uint32 + +var mods = [...]struct { + flags uint32 + code uint16 + mod key.Modifiers +}{ + // Left and right variants of modifier keys have their own masks, + // but they are not documented. These were determined empirically. + {1<<17 | 0x102, C.kVK_Shift, key.ModShift}, + {1<<17 | 0x104, C.kVK_RightShift, key.ModShift}, + {1<<18 | 0x101, C.kVK_Control, key.ModControl}, + // TODO key.ControlRight + {1<<19 | 0x120, C.kVK_Option, key.ModAlt}, + {1<<19 | 0x140, C.kVK_RightOption, key.ModAlt}, + {1<<20 | 0x108, C.kVK_Command, key.ModMeta}, + {1<<20 | 0x110, C.kVK_Command, key.ModMeta}, // TODO: missing kVK_RightCommand +} + +//export lifecycleAlive +func lifecycleAlive() { theApp.sendLifecycle(lifecycle.StageAlive) } + +//export lifecycleVisible +func lifecycleVisible() { + theApp.sendLifecycle(lifecycle.StageVisible) +} + +//export lifecycleFocused +func lifecycleFocused() { theApp.sendLifecycle(lifecycle.StageFocused) } + +// driverShowVirtualKeyboard does nothing on desktop +func driverShowVirtualKeyboard(KeyboardType) { +} + +// driverHideVirtualKeyboard does nothing on desktop +func driverHideVirtualKeyboard() { +} + +// driverShowFileOpenPicker does nothing on desktop +func driverShowFileOpenPicker(func(string, func()), *FileFilter) { +} + +// driverShowFileSavePicker does nothing on desktop +func driverShowFileSavePicker(func(string, func()), *FileFilter, string) { +} + +// convRune marks the Carbon/Cocoa private-range unicode rune representing +// a non-unicode key event to -1, used for Rune in the key package. +// +// http://www.unicode.org/Public/MAPPINGS/VENDORS/APPLE/CORPCHAR.TXT +func convRune(r rune) rune { + if '\uE000' <= r && r <= '\uF8FF' { + return -1 + } + return r +} + +var virtualKeyCodeMap = map[uint16]key.Code{ + C.kVK_ANSI_A: key.CodeA, + C.kVK_ANSI_B: key.CodeB, + C.kVK_ANSI_C: key.CodeC, + C.kVK_ANSI_D: key.CodeD, + C.kVK_ANSI_E: key.CodeE, + C.kVK_ANSI_F: key.CodeF, + C.kVK_ANSI_G: key.CodeG, + C.kVK_ANSI_H: key.CodeH, + C.kVK_ANSI_I: key.CodeI, + C.kVK_ANSI_J: key.CodeJ, + C.kVK_ANSI_K: key.CodeK, + C.kVK_ANSI_L: key.CodeL, + C.kVK_ANSI_M: key.CodeM, + C.kVK_ANSI_N: key.CodeN, + C.kVK_ANSI_O: key.CodeO, + C.kVK_ANSI_P: key.CodeP, + C.kVK_ANSI_Q: key.CodeQ, + C.kVK_ANSI_R: key.CodeR, + C.kVK_ANSI_S: key.CodeS, + C.kVK_ANSI_T: key.CodeT, + C.kVK_ANSI_U: key.CodeU, + C.kVK_ANSI_V: key.CodeV, + C.kVK_ANSI_W: key.CodeW, + C.kVK_ANSI_X: key.CodeX, + C.kVK_ANSI_Y: key.CodeY, + C.kVK_ANSI_Z: key.CodeZ, + C.kVK_ANSI_1: key.Code1, + C.kVK_ANSI_2: key.Code2, + C.kVK_ANSI_3: key.Code3, + C.kVK_ANSI_4: key.Code4, + C.kVK_ANSI_5: key.Code5, + C.kVK_ANSI_6: key.Code6, + C.kVK_ANSI_7: key.Code7, + C.kVK_ANSI_8: key.Code8, + C.kVK_ANSI_9: key.Code9, + C.kVK_ANSI_0: key.Code0, + // TODO: move the rest of these codes to constants in key.go + // if we are happy with them. + C.kVK_Return: key.CodeReturnEnter, + C.kVK_Escape: key.CodeEscape, + C.kVK_Delete: key.CodeDeleteBackspace, + C.kVK_Tab: key.CodeTab, + C.kVK_Space: key.CodeSpacebar, + C.kVK_ANSI_Minus: key.CodeHyphenMinus, + C.kVK_ANSI_Equal: key.CodeEqualSign, + C.kVK_ANSI_LeftBracket: key.CodeLeftSquareBracket, + C.kVK_ANSI_RightBracket: key.CodeRightSquareBracket, + C.kVK_ANSI_Backslash: key.CodeBackslash, + // 50: Keyboard Non-US "#" and ~ + C.kVK_ANSI_Semicolon: key.CodeSemicolon, + C.kVK_ANSI_Quote: key.CodeApostrophe, + C.kVK_ANSI_Grave: key.CodeGraveAccent, + C.kVK_ANSI_Comma: key.CodeComma, + C.kVK_ANSI_Period: key.CodeFullStop, + C.kVK_ANSI_Slash: key.CodeSlash, + C.kVK_CapsLock: key.CodeCapsLock, + C.kVK_F1: key.CodeF1, + C.kVK_F2: key.CodeF2, + C.kVK_F3: key.CodeF3, + C.kVK_F4: key.CodeF4, + C.kVK_F5: key.CodeF5, + C.kVK_F6: key.CodeF6, + C.kVK_F7: key.CodeF7, + C.kVK_F8: key.CodeF8, + C.kVK_F9: key.CodeF9, + C.kVK_F10: key.CodeF10, + C.kVK_F11: key.CodeF11, + C.kVK_F12: key.CodeF12, + // 70: PrintScreen + // 71: Scroll Lock + // 72: Pause + // 73: Insert + C.kVK_Home: key.CodeHome, + C.kVK_PageUp: key.CodePageUp, + C.kVK_ForwardDelete: key.CodeDeleteForward, + C.kVK_End: key.CodeEnd, + C.kVK_PageDown: key.CodePageDown, + C.kVK_RightArrow: key.CodeRightArrow, + C.kVK_LeftArrow: key.CodeLeftArrow, + C.kVK_DownArrow: key.CodeDownArrow, + C.kVK_UpArrow: key.CodeUpArrow, + C.kVK_ANSI_KeypadClear: key.CodeKeypadNumLock, + C.kVK_ANSI_KeypadDivide: key.CodeKeypadSlash, + C.kVK_ANSI_KeypadMultiply: key.CodeKeypadAsterisk, + C.kVK_ANSI_KeypadMinus: key.CodeKeypadHyphenMinus, + C.kVK_ANSI_KeypadPlus: key.CodeKeypadPlusSign, + C.kVK_ANSI_KeypadEnter: key.CodeKeypadEnter, + C.kVK_ANSI_Keypad1: key.CodeKeypad1, + C.kVK_ANSI_Keypad2: key.CodeKeypad2, + C.kVK_ANSI_Keypad3: key.CodeKeypad3, + C.kVK_ANSI_Keypad4: key.CodeKeypad4, + C.kVK_ANSI_Keypad5: key.CodeKeypad5, + C.kVK_ANSI_Keypad6: key.CodeKeypad6, + C.kVK_ANSI_Keypad7: key.CodeKeypad7, + C.kVK_ANSI_Keypad8: key.CodeKeypad8, + C.kVK_ANSI_Keypad9: key.CodeKeypad9, + C.kVK_ANSI_Keypad0: key.CodeKeypad0, + C.kVK_ANSI_KeypadDecimal: key.CodeKeypadFullStop, + C.kVK_ANSI_KeypadEquals: key.CodeKeypadEqualSign, + C.kVK_F13: key.CodeF13, + C.kVK_F14: key.CodeF14, + C.kVK_F15: key.CodeF15, + C.kVK_F16: key.CodeF16, + C.kVK_F17: key.CodeF17, + C.kVK_F18: key.CodeF18, + C.kVK_F19: key.CodeF19, + C.kVK_F20: key.CodeF20, + // 116: Keyboard Execute + C.kVK_Help: key.CodeHelp, + // 118: Keyboard Menu + // 119: Keyboard Select + // 120: Keyboard Stop + // 121: Keyboard Again + // 122: Keyboard Undo + // 123: Keyboard Cut + // 124: Keyboard Copy + // 125: Keyboard Paste + // 126: Keyboard Find + C.kVK_Mute: key.CodeMute, + C.kVK_VolumeUp: key.CodeVolumeUp, + C.kVK_VolumeDown: key.CodeVolumeDown, + // 130: Keyboard Locking Caps Lock + // 131: Keyboard Locking Num Lock + // 132: Keyboard Locking Scroll Lock + // 133: Keyboard Comma + // 134: Keyboard Equal Sign + // ...: Bunch of stuff + C.kVK_Control: key.CodeLeftControl, + C.kVK_Shift: key.CodeLeftShift, + C.kVK_Option: key.CodeLeftAlt, + C.kVK_Command: key.CodeLeftGUI, + C.kVK_RightControl: key.CodeRightControl, + C.kVK_RightShift: key.CodeRightShift, + C.kVK_RightOption: key.CodeRightAlt, +} + +// convVirtualKeyCode converts a Carbon/Cocoa virtual key code number +// into the standard keycodes used by the key package. +// +// To get a sense of the key map, see the diagram on +// +// http://boredzo.org/blog/archives/2007-05-22/virtual-key-codes +func convVirtualKeyCode(vkcode uint16) key.Code { + if code, ok := virtualKeyCodeMap[vkcode]; ok { + return code + } + + // TODO key.CodeRightGUI + return key.CodeUnknown +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/darwin_desktop.m b/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/darwin_desktop.m new file mode 100644 index 0000000..ab97f10 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/darwin_desktop.m @@ -0,0 +1,244 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build darwin && !ios + +#include "_cgo_export.h" +#include +#include + +#import +#import +#import + +void makeCurrentContext(GLintptr context) { + NSOpenGLContext* ctx = (NSOpenGLContext*)context; + [ctx makeCurrentContext]; +} + +uint64 threadID() { + uint64 id; + if (pthread_threadid_np(pthread_self(), &id)) { + abort(); + } + return id; +} + +@interface MobileGLView : NSOpenGLView +{ +} +@end + +@implementation MobileGLView +- (void)prepareOpenGL { + [super prepareOpenGL]; + [self setWantsBestResolutionOpenGLSurface:YES]; + GLint swapInt = 1; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [[self openGLContext] setValues:&swapInt forParameter:NSOpenGLCPSwapInterval]; +#pragma clang diagnostic pop + + // Using attribute arrays in OpenGL 3.3 requires the use of a VBA. + // But VBAs don't exist in ES 2. So we bind a default one. + GLuint vba; + glGenVertexArrays(1, &vba); + glBindVertexArray(vba); + + startloop((GLintptr)[self openGLContext]); +} + +- (void)reshape { + [super reshape]; + + // Calculate screen PPI. + // + // Note that the backingScaleFactor converts from logical + // pixels to actual pixels, but both of these units vary + // independently from real world size. E.g. + // + // 13" Retina Macbook Pro, 2560x1600, 227ppi, backingScaleFactor=2, scale=3.15 + // 15" Retina Macbook Pro, 2880x1800, 220ppi, backingScaleFactor=2, scale=3.06 + // 27" iMac, 2560x1440, 109ppi, backingScaleFactor=1, scale=1.51 + // 27" Retina iMac, 5120x2880, 218ppi, backingScaleFactor=2, scale=3.03 + NSScreen *screen = [NSScreen mainScreen]; + double screenPixW = [screen frame].size.width * [screen backingScaleFactor]; + + CGDirectDisplayID display = (CGDirectDisplayID)[[[screen deviceDescription] valueForKey:@"NSScreenNumber"] intValue]; + CGSize screenSizeMM = CGDisplayScreenSize(display); // in millimeters + float ppi = 25.4 * screenPixW / screenSizeMM.width; + float pixelsPerPt = ppi/72.0; + + // The width and height reported to the geom package are the + // bounds of the OpenGL view. Several steps are necessary. + // First, [self bounds] gives us the number of logical pixels + // in the view. Multiplying this by the backingScaleFactor + // gives us the number of actual pixels. + NSRect r = [self bounds]; + int w = r.size.width * [screen backingScaleFactor]; + int h = r.size.height * [screen backingScaleFactor]; + + setGeom(pixelsPerPt, w, h); +} + +- (void)drawRect:(NSRect)theRect { + // Called during resize. This gets rid of flicker when resizing. + drawgl(); +} + +- (void)mouseDown:(NSEvent *)theEvent { + double scale = [[NSScreen mainScreen] backingScaleFactor]; + NSPoint p = [theEvent locationInWindow]; + eventMouseDown(p.x * scale, p.y * scale); +} + +- (void)mouseUp:(NSEvent *)theEvent { + double scale = [[NSScreen mainScreen] backingScaleFactor]; + NSPoint p = [theEvent locationInWindow]; + eventMouseEnd(p.x * scale, p.y * scale); +} + +- (void)mouseDragged:(NSEvent *)theEvent { + double scale = [[NSScreen mainScreen] backingScaleFactor]; + NSPoint p = [theEvent locationInWindow]; + eventMouseDragged(p.x * scale, p.y * scale); +} + +- (void)windowDidBecomeKey:(NSNotification *)notification { + lifecycleFocused(); +} + +- (void)windowDidResignKey:(NSNotification *)notification { + if (![NSApp isHidden]) { + lifecycleVisible(); + } +} + +- (void)applicationDidFinishLaunching:(NSNotification *)aNotification { + lifecycleAlive(); + [[NSRunningApplication currentApplication] activateWithOptions:(NSApplicationActivateAllWindows | NSApplicationActivateIgnoringOtherApps)]; + [self.window makeKeyAndOrderFront:self]; + lifecycleVisible(); +} + +- (void)applicationWillTerminate:(NSNotification *)aNotification { + lifecycleDead(); +} + +- (void)applicationDidHide:(NSNotification *)aNotification { + lifecycleAlive(); +} + +- (void)applicationWillUnhide:(NSNotification *)notification { + lifecycleVisible(); +} + +- (void)windowWillClose:(NSNotification *)notification { + lifecycleDead(); +} + +- (BOOL)acceptsFirstResponder { + return true; +} + +- (void)keyDown:(NSEvent *)theEvent { + [self key:theEvent]; +} +- (void)keyUp:(NSEvent *)theEvent { + [self key:theEvent]; +} +- (void)key:(NSEvent *)theEvent { + NSRange range = [theEvent.characters rangeOfComposedCharacterSequenceAtIndex:0]; + + uint8_t buf[4] = {0, 0, 0, 0}; + if (![theEvent.characters getBytes:buf + maxLength:4 + usedLength:nil + encoding:NSUTF32LittleEndianStringEncoding + options:NSStringEncodingConversionAllowLossy + range:range + remainingRange:nil]) { + NSLog(@"failed to read key event %@", theEvent); + return; + } + + uint32_t rune = (uint32_t)buf[0]<<0 | (uint32_t)buf[1]<<8 | (uint32_t)buf[2]<<16 | (uint32_t)buf[3]<<24; + + uint8_t direction; + if ([theEvent isARepeat]) { + direction = 0; + } else if (theEvent.type == NSEventTypeKeyDown) { + direction = 1; + } else { + direction = 2; + } + eventKey((int32_t)rune, direction, theEvent.keyCode, theEvent.modifierFlags); +} + +- (void)flagsChanged:(NSEvent *)theEvent { + eventFlags(theEvent.modifierFlags); +} +@end + +void +runApp(void) { + [NSAutoreleasePool new]; + [NSApplication sharedApplication]; + [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; + + id menuBar = [[NSMenu new] autorelease]; + id menuItem = [[NSMenuItem new] autorelease]; + [menuBar addItem:menuItem]; + [NSApp setMainMenu:menuBar]; + + id menu = [[NSMenu new] autorelease]; + id name = [[NSProcessInfo processInfo] processName]; + + id hideMenuItem = [[[NSMenuItem alloc] initWithTitle:@"Hide" + action:@selector(hide:) keyEquivalent:@"h"] + autorelease]; + [menu addItem:hideMenuItem]; + + id quitMenuItem = [[[NSMenuItem alloc] initWithTitle:@"Quit" + action:@selector(terminate:) keyEquivalent:@"q"] + autorelease]; + [menu addItem:quitMenuItem]; + [menuItem setSubmenu:menu]; + + NSRect rect = NSMakeRect(0, 0, 600, 800); + + NSWindow* window = [[[NSWindow alloc] initWithContentRect:rect + styleMask:NSWindowStyleMaskTitled + backing:NSBackingStoreBuffered + defer:NO] + autorelease]; + window.styleMask |= NSWindowStyleMaskResizable; + window.styleMask |= NSWindowStyleMaskMiniaturizable; + window.styleMask |= NSWindowStyleMaskClosable; + window.title = name; + [window cascadeTopLeftFromPoint:NSMakePoint(20,20)]; + + NSOpenGLPixelFormatAttribute attr[] = { + NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion3_2Core, + NSOpenGLPFAColorSize, 24, + NSOpenGLPFAAlphaSize, 8, + NSOpenGLPFADepthSize, 16, + NSOpenGLPFAAccelerated, + NSOpenGLPFADoubleBuffer, + NSOpenGLPFAAllowOfflineRenderers, + 0 + }; + id pixFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attr]; + MobileGLView* view = [[MobileGLView alloc] initWithFrame:rect pixelFormat:pixFormat]; + [window setContentView:view]; + [window setDelegate:view]; + [NSApp setDelegate:view]; + + [NSApp run]; +} + +void stopApp(void) { + [NSApp terminate:nil]; +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/darwin_ios.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/darwin_ios.go new file mode 100644 index 0000000..c018acf --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/darwin_ios.go @@ -0,0 +1,330 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build darwin && ios + +package app + +/* +#cgo CFLAGS: -x objective-c -DGL_SILENCE_DEPRECATION +#cgo LDFLAGS: -framework Foundation -framework UIKit -framework MobileCoreServices -framework GLKit -framework OpenGLES -framework QuartzCore -framework UserNotifications +#include +#include +#include +#include +#include +#import + +extern struct utsname sysInfo; + +void runApp(void); +void makeCurrentContext(GLintptr ctx); +void swapBuffers(GLintptr ctx); +uint64_t threadID(); + +UIEdgeInsets getDevicePadding(); +bool isDark(); +void showKeyboard(int keyboardType); +void hideKeyboard(); + +void showFileOpenPicker(char* mimes, char *exts); +void showFileSavePicker(char* mimes, char *exts); +void closeFileResource(void* urlPtr); +*/ +import "C" + +import ( + "log" + "runtime" + "strings" + "time" + "unsafe" + + "fyne.io/fyne/v2/internal/driver/mobile/event/lifecycle" + "fyne.io/fyne/v2/internal/driver/mobile/event/paint" + "fyne.io/fyne/v2/internal/driver/mobile/event/size" + "fyne.io/fyne/v2/internal/driver/mobile/event/touch" +) + +var initThreadID uint64 + +func init() { + // Lock the goroutine responsible for initialization to an OS thread. + // This means the goroutine running main (and calling the run function + // below) is locked to the OS thread that started the program. This is + // necessary for the correct delivery of UIKit events to the process. + // + // A discussion on this topic: + // https://groups.google.com/forum/#!msg/golang-nuts/IiWZ2hUuLDA/SNKYYZBelsYJ + runtime.LockOSThread() + initThreadID = uint64(C.threadID()) +} + +func main(f func(App)) { + //if tid := uint64(C.threadID()); tid != initThreadID { + // log.Fatalf("app.Run called on thread %d, but app.init ran on %d", tid, initThreadID) + //} + + go func() { + f(theApp) + // TODO(crawshaw): trigger runApp to return + }() + C.runApp() + panic("unexpected return from app.runApp") +} + +var ( + pixelsPerPt float32 + screenScale int // [UIScreen mainScreen].scale, either 1, 2, or 3. +) + +var DisplayMetrics struct { + WidthPx int + HeightPx int +} + +func GoBack() { + // Apple do not permit apps to exit in any way other than user pressing home button / gesture +} + +//export setDisplayMetrics +func setDisplayMetrics(width, height int, scale int) { + DisplayMetrics.WidthPx = width + DisplayMetrics.HeightPx = height +} + +//export setScreen +func setScreen(scale int) { + C.uname(&C.sysInfo) + name := C.GoString(&C.sysInfo.machine[0]) + + var v float32 + + switch { + case strings.HasPrefix(name, "iPhone"): + v = 163 + case strings.HasPrefix(name, "iPad"): + // TODO: is there a better way to distinguish the iPad Mini? + switch name { + case "iPad2,5", "iPad2,6", "iPad2,7", "iPad4,4", "iPad4,5", "iPad4,6", "iPad4,7": + v = 163 // iPad Mini + default: + v = 132 + } + default: + v = 163 // names like i386 and x86_64 are the simulator + } + + if v == 0 { + log.Printf("unknown machine: %s", name) + v = 163 // emergency fallback + } + + pixelsPerPt = v * float32(scale) / 72 + screenScale = scale +} + +//export updateConfig +func updateConfig(width, height, orientation int32) { + o := size.OrientationUnknown + switch orientation { + case C.UIDeviceOrientationPortrait, C.UIDeviceOrientationPortraitUpsideDown: + o = size.OrientationPortrait + case C.UIDeviceOrientationLandscapeLeft, C.UIDeviceOrientationLandscapeRight: + o = size.OrientationLandscape + width, height = height, width + } + insets := C.getDevicePadding() + + theApp.events.In() <- size.Event{ + WidthPx: int(width), + HeightPx: int(height), + WidthPt: float32(width) / pixelsPerPt, + HeightPt: float32(height) / pixelsPerPt, + InsetTopPx: int(float32(insets.top) * float32(screenScale)), + InsetBottomPx: int(float32(insets.bottom) * float32(screenScale)), + InsetLeftPx: int(float32(insets.left) * float32(screenScale)), + InsetRightPx: int(float32(insets.right) * float32(screenScale)), + PixelsPerPt: pixelsPerPt, + Orientation: o, + DarkMode: bool(C.isDark()), + } + theApp.events.In() <- paint.Event{External: true} +} + +// touchIDs is the current active touches. The position in the array +// is the ID, the value is the UITouch* pointer value. +// +// It is widely reported that the iPhone can handle up to 5 simultaneous +// touch events, while the iPad can handle 11. +var touchIDs [11]uintptr + +//export sendTouch +func sendTouch(cTouch, cTouchType uintptr, x, y float32) { + id := -1 + for i, val := range touchIDs { + if val == cTouch { + id = i + break + } + } + if id == -1 { + for i, val := range touchIDs { + if val == 0 { + touchIDs[i] = cTouch + id = i + break + } + } + if id == -1 { + panic("out of touchIDs") + } + } + + t := touch.Type(cTouchType) + if t == touch.TypeEnd { + // Clear all touchIDs when touch ends. The UITouch pointers are unique + // at every multi-touch event. See: + // https://github.com/fyne-io/fyne/issues/2407 + // https://developer.apple.com/documentation/uikit/touches_presses_and_gestures?language=objc + for idx := range touchIDs { + touchIDs[idx] = 0 + } + } + + theApp.events.In() <- touch.Event{ + X: x, + Y: y, + Sequence: touch.Sequence(id), + Type: t, + } +} + +//export lifecycleDead +func lifecycleDead() { theApp.sendLifecycle(lifecycle.StageDead) } + +//export lifecycleAlive +func lifecycleAlive() { theApp.sendLifecycle(lifecycle.StageAlive) } + +//export lifecycleVisible +func lifecycleVisible() { theApp.sendLifecycle(lifecycle.StageVisible) } + +//export lifecycleFocused +func lifecycleFocused() { theApp.sendLifecycle(lifecycle.StageFocused) } + +//export lifecycleMemoryWarning +func lifecycleMemoryWarning() { + cleanCaches() +} + +//export drawloop +func drawloop() { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + for workAvailable := theApp.worker.WorkAvailable(); ; { + select { + case <-workAvailable: + theApp.worker.DoWork() + case <-theApp.publish: + theApp.publishResult <- PublishResult{} + return + case <-time.After(100 * time.Millisecond): // in case the method blocked!! + return + } + } +} + +//export startloop +func startloop(ctx C.GLintptr) { + go theApp.loop(ctx) +} + +// loop is the primary drawing loop. +// +// After UIKit has captured the initial OS thread for processing UIKit +// events in runApp, it starts loop on another goroutine. It is locked +// to an OS thread for its OpenGL context. +func (a *app) loop(ctx C.GLintptr) { + runtime.LockOSThread() + C.makeCurrentContext(ctx) + + workAvailable := a.worker.WorkAvailable() + + for { + select { + case <-workAvailable: + a.worker.DoWork() + case <-theApp.publish: + loop1: + for { + select { + case <-workAvailable: + a.worker.DoWork() + default: + break loop1 + } + } + C.swapBuffers(ctx) + theApp.publishResult <- PublishResult{} + } + } +} + +func cStringsForFilter(filter *FileFilter) (*C.char, *C.char) { + mimes := strings.Join(filter.MimeTypes, "|") + + // extensions must have the '.' removed for UTI lookups on iOS + extList := []string{} + for _, ext := range filter.Extensions { + extList = append(extList, ext[1:]) + } + exts := strings.Join(extList, "|") + + return C.CString(mimes), C.CString(exts) +} + +// driverShowVirtualKeyboard requests the driver to show a virtual keyboard for text input +func driverShowVirtualKeyboard(keyboard KeyboardType) { + C.showKeyboard(C.int(int32(keyboard))) +} + +// driverHideVirtualKeyboard requests the driver to hide any visible virtual keyboard +func driverHideVirtualKeyboard() { + C.hideKeyboard() +} + +var fileCallback func(string, func()) + +//export filePickerReturned +func filePickerReturned(str *C.char, urlPtr unsafe.Pointer) { + if fileCallback == nil { + return + } + + fileCallback(C.GoString(str), func() { + C.closeFileResource(urlPtr) + }) + fileCallback = nil +} + +func driverShowFileOpenPicker(callback func(string, func()), filter *FileFilter) { + fileCallback = callback + + mimeStr, extStr := cStringsForFilter(filter) + defer C.free(unsafe.Pointer(mimeStr)) + defer C.free(unsafe.Pointer(extStr)) + + C.showFileOpenPicker(mimeStr, extStr) +} + +func driverShowFileSavePicker(callback func(string, func()), filter *FileFilter, filename string) { + fileCallback = callback + + mimeStr, extStr := cStringsForFilter(filter) + defer C.free(unsafe.Pointer(mimeStr)) + defer C.free(unsafe.Pointer(extStr)) + + C.showFileSavePicker(mimeStr, extStr) +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/darwin_ios.m b/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/darwin_ios.m new file mode 100644 index 0000000..bfdfbde --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/darwin_ios.m @@ -0,0 +1,434 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build darwin && ios + +#include "_cgo_export.h" +#include +#include +#include + +#import +#import +#import +#import + +struct utsname sysInfo; + +static CGFloat keyboardHeight; + +@interface GoAppAppController : GLKViewController +@end + +@interface GoInputView : UITextField +@end + +@interface GoAppAppDelegate : UIResponder +@property (strong, nonatomic) UIWindow *window; +@property (strong, nonatomic) GoAppAppController *controller; +@end + +@implementation GoAppAppDelegate +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + int scale = 1; + if ([[UIScreen mainScreen] respondsToSelector:@selector(displayLinkWithTarget:selector:)]) { + scale = (int)[UIScreen mainScreen].scale; // either 1.0, 2.0, or 3.0. + } + CGSize size = [UIScreen mainScreen].nativeBounds.size; + setDisplayMetrics((int)size.width, (int)size.height, scale); + + lifecycleAlive(); + self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + self.controller = [[GoAppAppController alloc] initWithNibName:nil bundle:nil]; + self.window.rootViewController = self.controller; + [self.window makeKeyAndVisible]; + + // update insets once key window is set + UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation]; + updateConfig((int)size.width, (int)size.height, orientation); + + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + center.delegate = (id) self; + + return YES; +} + +- (void)applicationDidBecomeActive:(UIApplication * )application { + lifecycleFocused(); +} + +- (void)applicationWillResignActive:(UIApplication *)application { + lifecycleVisible(); +} + +- (void)applicationDidEnterBackground:(UIApplication *)application { + lifecycleAlive(); +} + +- (void)applicationWillTerminate:(UIApplication *)application { + lifecycleDead(); +} + +- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application { + lifecycleMemoryWarning(); +} + +- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray *)urls { + if ([urls count] == 0) { + return; + } + + NSURL* url = urls[0]; + NSURL* toClose = NULL; + BOOL secured = [url startAccessingSecurityScopedResource]; + if (secured) { + toClose = url; + } + + filePickerReturned((char*)[[url description] UTF8String], toClose); +} + +- (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller { + filePickerReturned("", NULL); +} + +- (void)userNotificationCenter:(UNUserNotificationCenter *)center + willPresentNotification:(UNNotification *)notification + withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler { + completionHandler(UNNotificationPresentationOptionAlert); +} +@end + +@interface GoAppAppController () +@property (strong, nonatomic) EAGLContext *context; +@property (strong, nonatomic) GLKView *glview; +@property (strong, nonatomic) GoInputView *inputView; +@end + +@implementation GoAppAppController +- (void)viewWillAppear:(BOOL)animated +{ + // TODO: replace by swapping out GLKViewController for a UIVIewController. + [super viewWillAppear:animated]; + self.paused = YES; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil]; +} + +- (void)viewWillDisappear:(BOOL)animated { + [super viewWillDisappear:animated]; + + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillHideNotification object:nil]; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; + self.inputView = [[GoInputView alloc] initWithFrame:CGRectMake(0, 0, 0, 0)]; + self.inputView.delegate = self.inputView; + self.inputView.autocapitalizationType = UITextAutocapitalizationTypeNone; + self.inputView.autocorrectionType = UITextAutocorrectionTypeNo; + [self.view addSubview:self.inputView]; + self.glview = (GLKView*)self.view; + self.glview.drawableDepthFormat = GLKViewDrawableDepthFormat24; + self.glview.multipleTouchEnabled = true; // TODO expose setting to user. + self.glview.context = self.context; + self.glview.userInteractionEnabled = YES; + //self.glview.enableSetNeedsDisplay = YES; // only invoked once + + // Do not use the GLKViewController draw loop. + //self.paused = YES; + //self.resumeOnDidBecomeActive = NO; + //self.preferredFramesPerSecond = 0; + + int scale = 1; + if ([[UIScreen mainScreen] respondsToSelector:@selector(displayLinkWithTarget:selector:)]) { + scale = (int)[UIScreen mainScreen].scale; // either 1.0, 2.0, or 3.0. + } + setScreen(scale); + + CGSize size = [UIScreen mainScreen].nativeBounds.size; + UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation]; + updateConfig((int)size.width, (int)size.height, orientation); + + self.glview.enableSetNeedsDisplay = NO; + CADisplayLink* displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(render:)]; + [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; +} + +- (void)viewWillTransitionToSize:(CGSize)ptSize withTransitionCoordinator:(id)coordinator { + [coordinator animateAlongsideTransition:^(id context) { + // TODO(crawshaw): come up with a plan to handle animations. + } completion:^(id context) { + UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation]; + CGSize size = [UIScreen mainScreen].nativeBounds.size; + updateConfig((int)size.width, (int)size.height, orientation); + }]; +} + +- (void)render:(CADisplayLink*)displayLink { + [self.glview display]; +} + +- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect { + drawloop(); +} + +#define TOUCH_TYPE_BEGIN 0 // touch.TypeBegin +#define TOUCH_TYPE_MOVE 1 // touch.TypeMove +#define TOUCH_TYPE_END 2 // touch.TypeEnd + +static void sendTouches(int change, NSSet* touches) { + CGFloat scale = [UIScreen mainScreen].nativeScale; + for (UITouch* touch in touches) { + CGPoint p = [touch locationInView:touch.view]; + sendTouch((GoUintptr)touch, (GoUintptr)change, p.x*scale, p.y*scale); + } +} + +- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event { + sendTouches(TOUCH_TYPE_BEGIN, touches); +} + +- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event { + sendTouches(TOUCH_TYPE_MOVE, touches); +} + +- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event { + sendTouches(TOUCH_TYPE_END, touches); +} + +- (void)touchesCanceled:(NSSet*)touches withEvent:(UIEvent*)event { + sendTouches(TOUCH_TYPE_END, touches); +} + +- (void) traitCollectionDidChange: (UITraitCollection *) previousTraitCollection { + [super traitCollectionDidChange: previousTraitCollection]; + + UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation]; + CGSize size = [UIScreen mainScreen].nativeBounds.size; + updateConfig((int)size.width, (int)size.height, orientation); +} + +- (void)keyboardWillShow:(NSNotification *)note { + CGSize keyboardSize = [[[note userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size; + keyboardHeight = keyboardSize.height; + + CGSize size = [UIScreen mainScreen].nativeBounds.size; + UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation]; + updateConfig((int)size.width, (int)size.height, orientation); +} + +- (void)keyboardWillHide:(NSNotification *)note { + keyboardHeight = 0; + + CGSize size = [UIScreen mainScreen].nativeBounds.size; + UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation]; + updateConfig((int)size.width, (int)size.height, orientation); +} + +@end + +@implementation GoInputView + +- (BOOL)canBecomeFirstResponder { + return YES; +} + +- (void)deleteBackward { + keyboardDelete(); +} + +-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string { + keyboardTyped((char *)[string UTF8String]); + return NO; +} + +- (BOOL)textFieldShouldReturn:(UITextField *)textField { + if ([self returnKeyType] != UIReturnKeyDone) { + keyboardTyped("\n"); + return YES; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + [self resignFirstResponder]; + }); + + return NO; +} + +@end + +void runApp(void) { + char * argv[] = {}; + @autoreleasepool { + UIApplicationMain(0, argv, nil, NSStringFromClass([GoAppAppDelegate class])); + } +} + +void makeCurrentContext(GLintptr context) { + EAGLContext* ctx = (EAGLContext*)context; + if (![EAGLContext setCurrentContext:ctx]) { + // TODO(crawshaw): determine how terrible this is. Exit? + NSLog(@"failed to set current context"); + } +} + +void swapBuffers(GLintptr context) { + __block EAGLContext* ctx = (EAGLContext*)context; + dispatch_sync(dispatch_get_main_queue(), ^{ + [EAGLContext setCurrentContext:ctx]; + [ctx presentRenderbuffer:GL_RENDERBUFFER]; + }); +} + +uint64_t threadID() { + uint64_t id; + if (pthread_threadid_np(pthread_self(), &id)) { + abort(); + } + return id; +} + +UIEdgeInsets getDevicePadding() { + if (@available(iOS 11.0, *)) { + UIWindow *window = UIApplication.sharedApplication.keyWindow; + + UIEdgeInsets inset = window.safeAreaInsets; + if (keyboardHeight != 0) { + inset.bottom = keyboardHeight; + } + return inset; + } + + return UIEdgeInsetsZero; +} + +bool isDark() { + UIViewController *rootVC = [[[[UIApplication sharedApplication] delegate] window] rootViewController]; + return rootVC.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark; +} + +#define DEFAULT_KEYBOARD_CODE 0 +#define SINGLELINE_KEYBOARD_CODE 1 +#define NUMBER_KEYBOARD_CODE 2 + +void showKeyboard(int keyboardType) { + GoAppAppDelegate *appDelegate = (GoAppAppDelegate *)[[UIApplication sharedApplication] delegate]; + GoInputView *view = appDelegate.controller.inputView; + + dispatch_async(dispatch_get_main_queue(), ^{ + switch (keyboardType) + { + case DEFAULT_KEYBOARD_CODE: + [view setKeyboardType:UIKeyboardTypeDefault]; + [view setReturnKeyType:UIReturnKeyDefault]; + break; + case SINGLELINE_KEYBOARD_CODE: + [view setKeyboardType:UIKeyboardTypeDefault]; + [view setReturnKeyType:UIReturnKeyDone]; + break; + case NUMBER_KEYBOARD_CODE: + [view setKeyboardType:UIKeyboardTypeNumberPad]; + [view setReturnKeyType:UIReturnKeyDone]; + break; + default: + NSLog(@"unknown keyboard type, use default"); + [view setKeyboardType:UIKeyboardTypeDefault]; + [view setReturnKeyType:UIReturnKeyDefault]; + break; + } + // refresh settings if keyboard is already open + [view reloadInputViews]; + + BOOL ret = [view becomeFirstResponder]; + }); +} + +void hideKeyboard() { + GoAppAppDelegate *appDelegate = (GoAppAppDelegate *)[[UIApplication sharedApplication] delegate]; + GoInputView *view = appDelegate.controller.inputView; + + dispatch_async(dispatch_get_main_queue(), ^{ + [view resignFirstResponder]; + }); +} + +NSMutableArray *docTypesForMimeExts(char *mimes, char *exts) { + NSMutableArray *docTypes = [NSMutableArray array]; + if (mimes != NULL && strlen(mimes) > 0) { + NSString *mimeList = [NSString stringWithUTF8String:mimes]; + + if ([mimeList isEqualToString:@"application/x-directory"]) { + [docTypes addObject:(NSString*)kUTTypeFolder]; + } else { + NSArray *mimeItems = [mimeList componentsSeparatedByString:@"|"]; + + for (NSString *mime in mimeItems) { + NSString *UTI = (NSString *) UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, (CFStringRef)mime, NULL); + + [docTypes addObject:UTI]; + } + } + } else if (exts != NULL && strlen(exts) > 0) { + NSString *extList = [NSString stringWithUTF8String:exts]; + NSArray *extItems = [extList componentsSeparatedByString:@"|"]; + + for (NSString *ext in extItems) { + NSString *UTI = (NSString *) UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (CFStringRef)ext, NULL); + + [docTypes addObject:UTI]; + } + } else { + [docTypes addObject:@"public.data"]; + } + + return docTypes; +} + +void showFileOpenPicker(char* mimes, char *exts) { + GoAppAppDelegate *appDelegate = (GoAppAppDelegate *)[[UIApplication sharedApplication] delegate]; + + NSMutableArray *docTypes = docTypesForMimeExts(mimes, exts); + + UIDocumentPickerViewController *documentPicker = [[UIDocumentPickerViewController alloc] + initWithDocumentTypes:docTypes inMode:UIDocumentPickerModeOpen]; + documentPicker.delegate = (id) appDelegate; + + dispatch_async(dispatch_get_main_queue(), ^{ + [appDelegate.controller presentViewController:documentPicker animated:YES completion:nil]; + }); +} + +void showFileSavePicker(char* mimes, char *exts) { + GoAppAppDelegate *appDelegate = (GoAppAppDelegate *)[[UIApplication sharedApplication] delegate]; + + NSMutableArray *docTypes = docTypesForMimeExts(mimes, exts); + + NSURL *temporaryDirectoryURL = [NSURL fileURLWithPath: NSTemporaryDirectory() isDirectory: YES]; + NSURL *temporaryFileURL = [temporaryDirectoryURL URLByAppendingPathComponent:@"filename"]; + + char* bytes = "\n"; + NSData *data = [NSData dataWithBytes:bytes length:1]; + BOOL ok = [data writeToURL:temporaryFileURL atomically:YES]; + + UIDocumentPickerViewController *documentPicker = [[UIDocumentPickerViewController alloc] + initWithURL:temporaryFileURL inMode:UIDocumentPickerModeMoveToService]; + documentPicker.delegate = (id) appDelegate; + + dispatch_async(dispatch_get_main_queue(), ^{ + [appDelegate.controller presentViewController:documentPicker animated:YES completion:nil]; + }); +} + +void closeFileResource(void* urlPtr) { + if (urlPtr == NULL) { + return; + } + + NSURL* url = (NSURL*) urlPtr; + [url stopAccessingSecurityScopedResource]; +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/doc.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/doc.go new file mode 100644 index 0000000..3ee2181 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/doc.go @@ -0,0 +1,88 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package app lets you write portable all-Go apps for Android and iOS. + +There are typically two ways to use Go on Android and iOS. The first +is to write a Go library and use `gomobile bind` to generate language +bindings for Java and Objective-C. Building a library does not +require the app package. The `gomobile bind` command produces output +that you can include in an Android Studio or Xcode project. For more +on language bindings, see https://golang.org/x/mobile/cmd/gobind. + +The second way is to write an app entirely in Go. The APIs are limited +to those that are portable between both Android and iOS, in particular +OpenGL, audio, and other Android NDK-like APIs. An all-Go app should +use this app package to initialize the app, manage its lifecycle, and +receive events. + +# Building apps + +Apps written entirely in Go have a main function, and can be built +with `gomobile build`, which directly produces runnable output for +Android and iOS. + +The gomobile tool can get installed with go get. For reference, see +https://golang.org/x/mobile/cmd/gomobile. + +For detailed instructions and documentation, see +https://golang.org/wiki/Mobile. + +# Event processing in Native Apps + +The Go runtime is initialized on Android when NativeActivity onCreate is +called, and on iOS when the process starts. In both cases, Go init functions +run before the app lifecycle has started. + +An app is expected to call the Main function in main.main. When the function +exits, the app exits. Inside the func passed to Main, call Filter on every +event received, and then switch on its type. Registered filters run when the +event is received, not when it is sent, so that filters run in the same +goroutine as other code that calls OpenGL. + + package main + + import ( + "log" + + "golang.org/x/mobile/app" + "golang.org/x/mobile/event/lifecycle" + "golang.org/x/mobile/event/paint" + ) + + func main() { + app.Main(func(a app.App) { + for e := range a.Events() { + switch e := a.Filter(e).(type) { + case lifecycle.Event: + // ... + case paint.Event: + log.Print("Call OpenGL here.") + a.Publish() + } + } + }) + } + +An event is represented by the any type, so any value can be an event. +Commonly used types include Event types defined by the following +packages: + - golang.org/x/mobile/event/lifecycle + - golang.org/x/mobile/event/mouse + - golang.org/x/mobile/event/paint + - golang.org/x/mobile/event/size + - golang.org/x/mobile/event/touch + +For example, touch.Event is the type that represents touch events. Other +packages may define their own events, and send them on an app's event channel. + +Other packages can also register event filters, e.g. to manage resources in +response to lifecycle events. Such packages should call: + + app.RegisterFilter(etc) + +in an init function inside that package. +*/ +package app // import "fyne.io/fyne/v2/internal/driver/mobile/app" diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/keyboard.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/keyboard.go new file mode 100644 index 0000000..9df0e07 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/keyboard.go @@ -0,0 +1,130 @@ +package app + +import "C" +import "fyne.io/fyne/v2/internal/driver/mobile/event/key" + +// KeyboardType represents the type of a keyboard +type KeyboardType int32 + +const ( + // DefaultKeyboard is the keyboard with default input style and "return" return key + DefaultKeyboard KeyboardType = iota + // SingleLineKeyboard is the keyboard with default input style and "Done" return key + SingleLineKeyboard + // NumberKeyboard is the keyboard with number input style and "Done" return key + NumberKeyboard +) + +//export keyboardTyped +func keyboardTyped(str *C.char) { + for _, r := range C.GoString(str) { + k := key.Event{ + Rune: r, + Code: getCodeFromRune(r), + Direction: key.DirPress, + } + theApp.events.In() <- k + + k.Direction = key.DirRelease + theApp.events.In() <- k + } +} + +//export keyboardDelete +func keyboardDelete() { + theApp.events.In() <- key.Event{ + Code: key.CodeDeleteBackspace, + Direction: key.DirPress, + Rune: '\x00', + } + theApp.events.In() <- key.Event{ + Code: key.CodeDeleteBackspace, + Direction: key.DirRelease, + Rune: '\x00', + } +} + +var codeRune = map[rune]key.Code{ + '0': key.Code0, + '1': key.Code1, + '2': key.Code2, + '3': key.Code3, + '4': key.Code4, + '5': key.Code5, + '6': key.Code6, + '7': key.Code7, + '8': key.Code8, + '9': key.Code9, + 'a': key.CodeA, + 'b': key.CodeB, + 'c': key.CodeC, + 'd': key.CodeD, + 'e': key.CodeE, + 'f': key.CodeF, + 'g': key.CodeG, + 'h': key.CodeH, + 'i': key.CodeI, + 'j': key.CodeJ, + 'k': key.CodeK, + 'l': key.CodeL, + 'm': key.CodeM, + 'n': key.CodeN, + 'o': key.CodeO, + 'p': key.CodeP, + 'q': key.CodeQ, + 'r': key.CodeR, + 's': key.CodeS, + 't': key.CodeT, + 'u': key.CodeU, + 'v': key.CodeV, + 'w': key.CodeW, + 'x': key.CodeX, + 'y': key.CodeY, + 'z': key.CodeZ, + 'A': key.CodeA, + 'B': key.CodeB, + 'C': key.CodeC, + 'D': key.CodeD, + 'E': key.CodeE, + 'F': key.CodeF, + 'G': key.CodeG, + 'H': key.CodeH, + 'I': key.CodeI, + 'J': key.CodeJ, + 'K': key.CodeK, + 'L': key.CodeL, + 'M': key.CodeM, + 'N': key.CodeN, + 'O': key.CodeO, + 'P': key.CodeP, + 'Q': key.CodeQ, + 'R': key.CodeR, + 'S': key.CodeS, + 'T': key.CodeT, + 'U': key.CodeU, + 'V': key.CodeV, + 'W': key.CodeW, + 'X': key.CodeX, + 'Y': key.CodeY, + 'Z': key.CodeZ, + ',': key.CodeComma, + '.': key.CodeFullStop, + ' ': key.CodeSpacebar, + '\n': key.CodeReturnEnter, + '`': key.CodeGraveAccent, + '-': key.CodeHyphenMinus, + '=': key.CodeEqualSign, + '[': key.CodeLeftSquareBracket, + ']': key.CodeRightSquareBracket, + '\\': key.CodeBackslash, + ';': key.CodeSemicolon, + '\'': key.CodeApostrophe, + '/': key.CodeSlash, +} + +func getCodeFromRune(r rune) key.Code { + if code, ok := codeRune[r]; ok { + return code + } + return key.CodeUnknown +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/mobile.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/mobile.go new file mode 100644 index 0000000..d6dbf84 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/mobile.go @@ -0,0 +1,16 @@ +//go:build ios || android + +package app + +import "C" + +import ( + "runtime" + "runtime/debug" +) + +// clean caches - called when the OS wants some memory back +func cleanCaches() { + runtime.GC() + debug.FreeOSMemory() +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/shiny.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/shiny.go new file mode 100644 index 0000000..b2c3afe --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/shiny.go @@ -0,0 +1,33 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build windows + +package app + +import "log" + +func main(f func(a App)) { + log.Fatalln("Running mobile simulation mode does not currently work on Windows.") +} + +func GoBack() { + // When simulating mobile there are no other activities open (and we can't just force background) +} + +// driverShowVirtualKeyboard does nothing on desktop +func driverShowVirtualKeyboard(KeyboardType) { +} + +// driverHideVirtualKeyboard does nothing on desktop +func driverHideVirtualKeyboard() { +} + +// driverShowFileOpenPicker does nothing on desktop +func driverShowFileOpenPicker(func(string, func()), *FileFilter) { +} + +// driverShowFileSavePicker does nothing on desktop +func driverShowFileSavePicker(func(string, func()), *FileFilter, string) { +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/x11.c b/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/x11.c new file mode 100644 index 0000000..c83b2fd --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/x11.c @@ -0,0 +1,174 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build (linux && !android) || freebsd || openbsd + +#include "_cgo_export.h" +#include +#include +#include +#include +#include +#include + +static Atom wm_delete_window; + +static Window +new_window(Display *x_dpy, EGLDisplay e_dpy, int w, int h, EGLContext *ctx, EGLSurface *surf) { + static const EGLint attribs[] = { + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL_SURFACE_TYPE, EGL_WINDOW_BIT, + EGL_BLUE_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_RED_SIZE, 8, + EGL_DEPTH_SIZE, 16, + EGL_CONFIG_CAVEAT, EGL_NONE, + EGL_NONE + }; + EGLConfig config; + EGLint num_configs; + if (!eglChooseConfig(e_dpy, attribs, &config, 1, &num_configs)) { + fprintf(stderr, "eglChooseConfig failed\n"); + exit(1); + } + EGLint vid; + if (!eglGetConfigAttrib(e_dpy, config, EGL_NATIVE_VISUAL_ID, &vid)) { + fprintf(stderr, "eglGetConfigAttrib failed\n"); + exit(1); + } + + XVisualInfo visTemplate; + visTemplate.visualid = vid; + int num_visuals; + XVisualInfo *visInfo = XGetVisualInfo(x_dpy, VisualIDMask, &visTemplate, &num_visuals); + if (!visInfo) { + fprintf(stderr, "XGetVisualInfo failed\n"); + exit(1); + } + + Window root = RootWindow(x_dpy, DefaultScreen(x_dpy)); + XSetWindowAttributes attr; + + attr.colormap = XCreateColormap(x_dpy, root, visInfo->visual, AllocNone); + if (!attr.colormap) { + fprintf(stderr, "XCreateColormap failed\n"); + exit(1); + } + + attr.event_mask = StructureNotifyMask | ExposureMask | + ButtonPressMask | ButtonReleaseMask | ButtonMotionMask; + Window win = XCreateWindow( + x_dpy, root, 0, 0, w, h, 0, visInfo->depth, InputOutput, + visInfo->visual, CWColormap | CWEventMask, &attr); + XFree(visInfo); + + XSizeHints sizehints; + sizehints.width = w; + sizehints.height = h; + sizehints.flags = USSize; + XSetNormalHints(x_dpy, win, &sizehints); + XSetStandardProperties(x_dpy, win, "App", "App", None, (char **)NULL, 0, &sizehints); + + static const EGLint ctx_attribs[] = { + EGL_CONTEXT_CLIENT_VERSION, 2, + EGL_NONE + }; + *ctx = eglCreateContext(e_dpy, config, EGL_NO_CONTEXT, ctx_attribs); + if (!*ctx) { + fprintf(stderr, "eglCreateContext failed\n"); + exit(1); + } + *surf = eglCreateWindowSurface(e_dpy, config, win, NULL); + if (!*surf) { + fprintf(stderr, "eglCreateWindowSurface failed\n"); + exit(1); + } + return win; +} + +Display *x_dpy; +EGLDisplay e_dpy; +EGLContext e_ctx; +EGLSurface e_surf; +Window win; + +void +createWindow(void) { + x_dpy = XOpenDisplay(NULL); + if (!x_dpy) { + fprintf(stderr, "XOpenDisplay failed\n"); + exit(1); + } + e_dpy = eglGetDisplay(x_dpy); + if (!e_dpy) { + fprintf(stderr, "eglGetDisplay failed\n"); + exit(1); + } + EGLint e_major, e_minor; + if (!eglInitialize(e_dpy, &e_major, &e_minor)) { + fprintf(stderr, "eglInitialize failed\n"); + exit(1); + } + eglBindAPI(EGL_OPENGL_ES_API); + win = new_window(x_dpy, e_dpy, 600, 800, &e_ctx, &e_surf); + + wm_delete_window = XInternAtom(x_dpy, "WM_DELETE_WINDOW", True); + if (wm_delete_window != None) { + XSetWMProtocols(x_dpy, win, &wm_delete_window, 1); + } + + XMapWindow(x_dpy, win); + if (!eglMakeCurrent(e_dpy, e_surf, e_surf, e_ctx)) { + fprintf(stderr, "eglMakeCurrent failed\n"); + exit(1); + } + + // Window size and DPI should be initialized before starting app. + XEvent ev; + while (1) { + if (XCheckMaskEvent(x_dpy, StructureNotifyMask, &ev) == False) { + continue; + } + if (ev.type == ConfigureNotify) { + onResize(ev.xconfigure.width, ev.xconfigure.height); + break; + } + } +} + +void +processEvents(void) { + while (XPending(x_dpy)) { + XEvent ev; + XNextEvent(x_dpy, &ev); + switch (ev.type) { + case ButtonPress: + onTouchBegin((float)ev.xbutton.x, (float)ev.xbutton.y); + break; + case ButtonRelease: + onTouchEnd((float)ev.xbutton.x, (float)ev.xbutton.y); + break; + case MotionNotify: + onTouchMove((float)ev.xmotion.x, (float)ev.xmotion.y); + break; + case ConfigureNotify: + onResize(ev.xconfigure.width, ev.xconfigure.height); + break; + case ClientMessage: + if (wm_delete_window != None && (Atom)ev.xclient.data.l[0] == wm_delete_window) { + onStop(); + return; + } + break; + } + } +} + +void +swapBuffers(void) { + if (eglSwapBuffers(e_dpy, e_surf) == EGL_FALSE) { + fprintf(stderr, "eglSwapBuffer failed\n"); + exit(1); + } +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/x11.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/x11.go new file mode 100644 index 0000000..ad40c8a --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/app/x11.go @@ -0,0 +1,147 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build (linux && !android) || freebsd || openbsd + +package app + +/* +Simple on-screen app debugging for X11. Not an officially supported +development target for apps, as screens with mice are very different +than screens with touch panels. +*/ + +/* +#cgo LDFLAGS: -lEGL -lGLESv2 -lX11 +#cgo freebsd CFLAGS: -I/usr/local/include/ +#cgo openbsd CFLAGS: -I/usr/X11R6/include/ + +void createWindow(void); +void processEvents(void); +void swapBuffers(void); +*/ +import "C" + +import ( + "runtime" + "time" + + "fyne.io/fyne/v2/internal/driver/mobile/event/lifecycle" + "fyne.io/fyne/v2/internal/driver/mobile/event/paint" + "fyne.io/fyne/v2/internal/driver/mobile/event/size" + "fyne.io/fyne/v2/internal/driver/mobile/event/touch" +) + +func init() { + theApp.registerGLViewportFilter() +} + +func main(f func(App)) { + runtime.LockOSThread() + + workAvailable := theApp.worker.WorkAvailable() + heartbeat := time.NewTicker(time.Second / 60) + + C.createWindow() + + // TODO: send lifecycle events when e.g. the X11 window is iconified or moved off-screen. + theApp.sendLifecycle(lifecycle.StageFocused) + + // TODO: translate X11 expose events to shiny paint events, instead of + // sending this synthetic paint event as a hack. + theApp.events.In() <- paint.Event{} + + donec := make(chan struct{}) + go func() { + f(theApp) + close(donec) + }() + + // TODO: can we get the actual vsync signal? + ticker := time.NewTicker(time.Second / 60) + defer ticker.Stop() + var tc <-chan time.Time + + for { + select { + case <-donec: + return + case <-heartbeat.C: + C.processEvents() + case <-workAvailable: + theApp.worker.DoWork() + case <-theApp.publish: + C.swapBuffers() + tc = ticker.C + case <-tc: + tc = nil + theApp.publishResult <- PublishResult{} + } + } +} + +func GoBack() { + // When simulating mobile there are no other activities open (and we can't just force background) +} + +//export onResize +func onResize(w, h int) { + // TODO(nigeltao): don't assume 72 DPI. DisplayWidth and DisplayWidthMM + // is probably the best place to start looking. + pixelsPerPt := float32(1) + theApp.events.In() <- size.Event{ + WidthPx: w, + HeightPx: h, + WidthPt: float32(w), + HeightPt: float32(h), + PixelsPerPt: pixelsPerPt, + Orientation: screenOrientation(w, h), + } +} + +func sendTouch(t touch.Type, x, y float32) { + theApp.events.In() <- touch.Event{ + X: x, + Y: y, + Sequence: 0, // TODO: button?? + Type: t, + } +} + +//export onTouchBegin +func onTouchBegin(x, y float32) { sendTouch(touch.TypeBegin, x, y) } + +//export onTouchMove +func onTouchMove(x, y float32) { sendTouch(touch.TypeMove, x, y) } + +//export onTouchEnd +func onTouchEnd(x, y float32) { sendTouch(touch.TypeEnd, x, y) } + +var stopped bool + +//export onStop +func onStop() { + if stopped { + return + } + stopped = true + theApp.sendLifecycle(lifecycle.StageDead) + theApp.events.Close() +} + +// driverShowVirtualKeyboard does nothing on desktop +func driverShowVirtualKeyboard(KeyboardType) { +} + +// driverHideVirtualKeyboard does nothing on desktop +func driverHideVirtualKeyboard() { +} + +// driverShowFileOpenPicker does nothing on desktop +func driverShowFileOpenPicker(func(string, func()), *FileFilter) { +} + +// driverShowFileSavePicker does nothing on desktop +func driverShowFileSavePicker(func(string, func()), *FileFilter, string) { +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/canvas.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/canvas.go new file mode 100644 index 0000000..f37a5a7 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/canvas.go @@ -0,0 +1,428 @@ +package mobile + +import ( + "context" + "image" + "math" + "sync" + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/driver/mobile" + "fyne.io/fyne/v2/internal/app" + intdriver "fyne.io/fyne/v2/internal/driver" + "fyne.io/fyne/v2/internal/driver/common" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" +) + +var _ fyne.Canvas = (*canvas)(nil) + +type canvas struct { + common.Canvas + content fyne.CanvasObject + device *device + initialized bool + lastTapDown map[int]time.Time + lastTapDownPos map[int]fyne.Position + lastTapDelta map[int]fyne.Delta + menu fyne.CanvasObject + padded bool + scale float32 + size fyne.Size + touched map[int]mobile.Touchable + windowHead fyne.CanvasObject + + dragOffset fyne.Position + dragStart fyne.Position + dragging fyne.Draggable + + onTypedKey func(event *fyne.KeyEvent) + onTypedRune func(rune) + + touchCancelFunc context.CancelFunc + touchCancelLock sync.Mutex + touchLastTapped fyne.CanvasObject + touchTapCount int +} + +func newCanvas(dev fyne.Device) fyne.Canvas { + d, _ := dev.(*device) + ret := &canvas{ + Canvas: common.Canvas{ + OnFocus: d.handleKeyboard, + OnUnfocus: d.hideVirtualKeyboard, + }, + device: d, + lastTapDown: make(map[int]time.Time), + lastTapDownPos: make(map[int]fyne.Position), + lastTapDelta: make(map[int]fyne.Delta), + padded: true, + scale: dev.SystemScaleForWindow(nil), // we don't need a window parameter on mobile, + touched: make(map[int]mobile.Touchable), + } + ret.Initialize(ret, ret.overlayChanged) + return ret +} + +func (c *canvas) Capture() image.Image { + return c.Painter().Capture(c) +} + +func (c *canvas) Content() fyne.CanvasObject { + return c.content +} + +func (c *canvas) InteractiveArea() (fyne.Position, fyne.Size) { + var pos fyne.Position + var size fyne.Size + if c.device == nil { + // running in test mode + size = c.Size() + } else { + safeLeft := float32(c.device.safeLeft) / c.scale + safeTop := float32(c.device.safeTop) / c.scale + safeRight := float32(c.device.safeRight) / c.scale + safeBottom := float32(c.device.safeBottom) / c.scale + pos = fyne.NewPos(safeLeft, safeTop) + size = c.size.SubtractWidthHeight(safeLeft+safeRight, safeTop+safeBottom) + } + if c.windowHeadIsDisplacing() { + offset := c.windowHead.MinSize().Height + pos = pos.AddXY(0, offset) + size = size.SubtractWidthHeight(0, offset) + } + return pos, size +} + +func (c *canvas) MinSize() fyne.Size { + return c.size // TODO check +} + +func (c *canvas) OnTypedKey() func(*fyne.KeyEvent) { + return c.onTypedKey +} + +func (c *canvas) OnTypedRune() func(rune) { + return c.onTypedRune +} + +func (c *canvas) PixelCoordinateForPosition(pos fyne.Position) (int, int) { + return int(pos.X * c.scale), int(pos.Y * c.scale) +} + +func (c *canvas) Resize(size fyne.Size) { + if size == c.size { + return + } + + c.sizeContent(size) +} + +func (c *canvas) Scale() float32 { + return c.scale +} + +func (c *canvas) SetContent(content fyne.CanvasObject) { + c.setContent(content) + c.sizeContent(c.Size()) // fixed window size for mobile, cannot stretch to new content + c.SetDirty() +} + +func (c *canvas) SetOnTypedKey(typed func(*fyne.KeyEvent)) { + c.onTypedKey = typed +} + +func (c *canvas) SetOnTypedRune(typed func(rune)) { + c.onTypedRune = typed +} + +func (c *canvas) Size() fyne.Size { + return c.size +} + +func (c *canvas) applyThemeOutOfTreeObjects() { + if c.menu != nil { + app.ApplyThemeTo(c.menu, c) // Ensure our menu gets the theme change message as it's out-of-tree + } + if c.windowHead != nil { + app.ApplyThemeTo(c.windowHead, c) // Ensure our child windows get the theme change message as it's out-of-tree + } +} + +func (c *canvas) findObjectAtPositionMatching(pos fyne.Position, test func(object fyne.CanvasObject) bool) (fyne.CanvasObject, fyne.Position, int) { + if c.menu != nil { + return intdriver.FindObjectAtPositionMatching(pos, test, c.Overlays().Top(), c.menu) + } + + return intdriver.FindObjectAtPositionMatching(pos, test, c.Overlays().Top(), c.windowHead, c.content) +} + +func (c *canvas) overlayChanged() { + c.device.handleKeyboard(c.Focused()) + c.SetDirty() +} + +func (c *canvas) setContent(content fyne.CanvasObject) { + c.content = content + c.SetContentTreeAndFocusMgr(content) +} + +func (c *canvas) setMenu(menu fyne.CanvasObject) { + c.menu = menu + c.SetMenuTreeAndFocusMgr(menu) +} + +func (c *canvas) setWindowHead(head fyne.CanvasObject) { + if c.padded { + head = container.NewPadded(head) + } + c.windowHead = head + c.SetMobileWindowHeadTree(head) +} + +func (c *canvas) sizeContent(size fyne.Size) { + if c.content == nil { // window may not be configured yet + return + } + + c.size = size + areaPos, areaSize := c.InteractiveArea() + + if c.windowHead != nil { + var headSize fyne.Size + headPos := areaPos + if c.windowHeadIsDisplacing() { + headSize = fyne.NewSize(areaSize.Width, c.windowHead.MinSize().Height) + headPos = headPos.SubtractXY(0, headSize.Height) + } else { + headSize = c.windowHead.MinSize() + } + c.windowHead.Resize(headSize) + c.windowHead.Move(headPos) + } + + for _, overlay := range c.Overlays().List() { + if p, ok := overlay.(*widget.PopUp); ok { + // TODO: remove this when #707 is being addressed. + // “Notifies” the PopUp of the canvas size change. + p.Refresh() + } else { + overlay.Resize(areaSize) + overlay.Move(areaPos) + } + } + + if c.padded { + c.content.Resize(areaSize.Subtract(fyne.NewSize(theme.Padding()*2, theme.Padding()*2))) + c.content.Move(areaPos.Add(fyne.NewPos(theme.Padding(), theme.Padding()))) + } else { + c.content.Resize(areaSize) + c.content.Move(areaPos) + } +} + +func (c *canvas) tapDown(pos fyne.Position, tapID int) { + c.lastTapDown[tapID] = time.Now() + c.lastTapDownPos[tapID] = pos + c.dragging = nil + + co, objPos, layer := c.findObjectAtPositionMatching(pos, func(object fyne.CanvasObject) bool { + switch object.(type) { + case mobile.Touchable, fyne.Focusable: + return true + } + + return false + }) + + if wid, ok := co.(mobile.Touchable); ok { + touchEv := &mobile.TouchEvent{} + touchEv.Position = objPos + touchEv.AbsolutePosition = pos + wid.TouchDown(touchEv) + c.touched[tapID] = wid + } + + if layer != 1 { // 0 - overlay, 1 - window head / menu, 2 - content + if wid, ok := co.(fyne.Focusable); !ok || wid != c.Focused() { + c.Unfocus() + } + } +} + +func (c *canvas) tapMove(pos fyne.Position, tapID int, + dragCallback func(fyne.Draggable, *fyne.DragEvent), +) { + previousPos := c.lastTapDownPos[tapID] + deltaX := pos.X - previousPos.X + deltaY := pos.Y - previousPos.Y + + if c.dragging == nil && (math.Abs(float64(deltaX)) < tapMoveThreshold && math.Abs(float64(deltaY)) < tapMoveThreshold) { + return + } + c.lastTapDownPos[tapID] = pos + offset := fyne.Delta{DX: deltaX, DY: deltaY} + c.lastTapDelta[tapID] = offset + + co, objPos, _ := c.findObjectAtPositionMatching(pos, func(object fyne.CanvasObject) bool { + if _, ok := object.(fyne.Draggable); ok { + return true + } else if _, ok := object.(mobile.Touchable); ok { + return true + } + + return false + }) + + if c.touched[tapID] != nil { + if touch, ok := co.(mobile.Touchable); !ok || c.touched[tapID] != touch { + touchEv := &mobile.TouchEvent{} + touchEv.Position = objPos + touchEv.AbsolutePosition = pos + c.touched[tapID].TouchCancel(touchEv) + c.touched[tapID] = nil + } + } + + if c.dragging == nil { + if drag, ok := co.(fyne.Draggable); ok { + c.dragging = drag + c.dragOffset = previousPos.Subtract(objPos) + c.dragStart = co.Position() + } else { + return + } + } + + ev := &fyne.DragEvent{} + draggedObjDelta := c.dragStart.Subtract(c.dragging.(fyne.CanvasObject).Position()) + ev.Position = pos.Subtract(c.dragOffset).Add(draggedObjDelta) + ev.Dragged = offset + + dragCallback(c.dragging, ev) +} + +func (c *canvas) tapUp(pos fyne.Position, tapID int, + tapCallback func(fyne.Tappable, *fyne.PointEvent), + tapAltCallback func(fyne.SecondaryTappable, *fyne.PointEvent), + doubleTapCallback func(fyne.DoubleTappable, *fyne.PointEvent), + dragCallback func(fyne.Draggable, *fyne.DragEvent), +) { + if c.dragging != nil { + previousDelta := c.lastTapDelta[tapID] + ev := &fyne.DragEvent{Dragged: previousDelta} + draggedObjDelta := c.dragStart.Subtract(c.dragging.(fyne.CanvasObject).Position()) + ev.Position = pos.Subtract(c.dragOffset).Add(draggedObjDelta) + ev.AbsolutePosition = pos + dragCallback(c.dragging, ev) + + c.dragging = nil + return + } + + duration := time.Since(c.lastTapDown[tapID]) + + if c.menu != nil && c.Overlays().Top() == nil && pos.X > c.menu.Size().Width { + c.menu.Hide() + c.menu.Refresh() + c.setMenu(nil) + return + } + + co, objPos, _ := c.findObjectAtPositionMatching(pos, func(object fyne.CanvasObject) bool { + if _, ok := object.(fyne.Tappable); ok { + return true + } else if _, ok := object.(fyne.SecondaryTappable); ok { + return true + } else if _, ok := object.(mobile.Touchable); ok { + return true + } else if _, ok := object.(fyne.DoubleTappable); ok { + return true + } + + return false + }) + + if wid, ok := co.(mobile.Touchable); ok { + touchEv := &mobile.TouchEvent{} + touchEv.Position = objPos + touchEv.AbsolutePosition = pos + wid.TouchUp(touchEv) + c.touched[tapID] = nil + } + + ev := &fyne.PointEvent{ + Position: objPos, + AbsolutePosition: pos, + } + + if duration < tapSecondaryDelay { + _, doubleTap := co.(fyne.DoubleTappable) + if doubleTap { + c.touchCancelLock.Lock() + c.touchTapCount++ + c.touchLastTapped = co + cancel := c.touchCancelFunc + c.touchCancelLock.Unlock() + if cancel != nil { + cancel() + return + } + go c.waitForDoubleTap(co, ev, tapCallback, doubleTapCallback) + } else { + if wid, ok := co.(fyne.Tappable); ok { + tapCallback(wid, ev) + } + } + } else { + if wid, ok := co.(fyne.SecondaryTappable); ok { + tapAltCallback(wid, ev) + } + } +} + +func (c *canvas) waitForDoubleTap(co fyne.CanvasObject, ev *fyne.PointEvent, tapCallback func(fyne.Tappable, *fyne.PointEvent), doubleTapCallback func(fyne.DoubleTappable, *fyne.PointEvent)) { + ctx, cancel := context.WithDeadline(context.TODO(), time.Now().Add(tapDoubleDelay)) + c.touchCancelLock.Lock() + c.touchCancelFunc = cancel + c.touchCancelLock.Unlock() + defer cancel() + + <-ctx.Done() + fyne.CurrentApp().Driver().DoFromGoroutine(func() { + c.touchCancelLock.Lock() + touchCount := c.touchTapCount + touchLast := c.touchLastTapped + c.touchCancelLock.Unlock() + + if touchCount == 2 && touchLast == co { + if wid, ok := co.(fyne.DoubleTappable); ok { + doubleTapCallback(wid, ev) + } + } else { + if wid, ok := co.(fyne.Tappable); ok { + tapCallback(wid, ev) + } + } + + c.touchCancelLock.Lock() + c.touchTapCount = 0 + c.touchCancelFunc = nil + c.touchLastTapped = nil + c.touchCancelLock.Unlock() + }, true) +} + +func (c *canvas) windowHeadIsDisplacing() bool { + if c.windowHead == nil { + return false + } + + chromeBox := c.windowHead.(*fyne.Container) + if c.padded { + chromeBox = chromeBox.Objects[0].(*fyne.Container) // the padded container + } + return len(chromeBox.Objects) > 1 +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/clipboard.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/clipboard.go new file mode 100644 index 0000000..48f3f08 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/clipboard.go @@ -0,0 +1,15 @@ +package mobile + +import ( + "fyne.io/fyne/v2" +) + +// Declare conformity with Clipboard interface +var _ fyne.Clipboard = mobileClipboard{} + +func NewClipboard() fyne.Clipboard { + return mobileClipboard{} +} + +// mobileClipboard represents the system mobileClipboard +type mobileClipboard struct{} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/clipboard_android.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/clipboard_android.go new file mode 100644 index 0000000..cbb801d --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/clipboard_android.go @@ -0,0 +1,46 @@ +//go:build android + +package mobile + +/* +#cgo LDFLAGS: -landroid -llog -lEGL -lGLESv2 + +#include + +char *getClipboardContent(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx); +void setClipboardContent(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx, char *content); +*/ +import "C" + +import ( + "unsafe" + + "fyne.io/fyne/v2/internal/driver/mobile/app" +) + +// Content returns the clipboard content for Android +func (c mobileClipboard) Content() string { + content := "" + app.RunOnJVM(func(vm, env, ctx uintptr) error { + chars := C.getClipboardContent(C.uintptr_t(vm), C.uintptr_t(env), C.uintptr_t(ctx)) + if chars == nil { + return nil + } + + content = C.GoString(chars) + C.free(unsafe.Pointer(chars)) + return nil + }) + return content +} + +// SetContent sets the clipboard content for Android +func (c mobileClipboard) SetContent(content string) { + contentStr := C.CString(content) + defer C.free(unsafe.Pointer(contentStr)) + + app.RunOnJVM(func(vm, env, ctx uintptr) error { + C.setClipboardContent(C.uintptr_t(vm), C.uintptr_t(env), C.uintptr_t(ctx), contentStr) + return nil + }) +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/clipboard_desktop.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/clipboard_desktop.go new file mode 100644 index 0000000..76e8b91 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/clipboard_desktop.go @@ -0,0 +1,16 @@ +//go:build !ios && !android + +package mobile + +import "fyne.io/fyne/v2" + +// Content returns the clipboard content for mobile simulator runs +func (c mobileClipboard) Content() string { + fyne.LogError("Clipboard is not supported in mobile simulation", nil) + return "" +} + +// SetContent sets the clipboard content for mobile simulator runs +func (c mobileClipboard) SetContent(content string) { + fyne.LogError("Clipboard is not supported in mobile simulation", nil) +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/clipboard_ios.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/clipboard_ios.go new file mode 100644 index 0000000..1ff2c99 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/clipboard_ios.go @@ -0,0 +1,30 @@ +//go:build ios + +package mobile + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Foundation -framework UIKit -framework MobileCoreServices + +#include + +void setClipboardContent(char *content); +char *getClipboardContent(); +*/ +import "C" +import "unsafe" + +// Content returns the clipboard content for iOS +func (c mobileClipboard) Content() string { + content := C.getClipboardContent() + + return C.GoString(content) +} + +// SetContent sets the clipboard content for iOS +func (c mobileClipboard) SetContent(content string) { + contentStr := C.CString(content) + defer C.free(unsafe.Pointer(contentStr)) + + C.setClipboardContent(contentStr) +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/clipboard_ios.m b/vendor/fyne.io/fyne/v2/internal/driver/mobile/clipboard_ios.m new file mode 100644 index 0000000..ea3827b --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/clipboard_ios.m @@ -0,0 +1,15 @@ +//go:build ios + +#import +#import + +void setClipboardContent(char *content) { + NSString *value = [NSString stringWithUTF8String:content]; + [[UIPasteboard generalPasteboard] setString:value]; +} + +char *getClipboardContent() { + NSString *str = [[UIPasteboard generalPasteboard] string]; + + return (char *) [str UTF8String]; +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/device.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/device.go new file mode 100644 index 0000000..ce24d92 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/device.go @@ -0,0 +1,60 @@ +package mobile + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/driver/mobile" + "fyne.io/fyne/v2/internal/driver/mobile/event/size" + "fyne.io/fyne/v2/lang" +) + +type device struct { + safeTop, safeLeft, safeBottom, safeRight int + + keyboardShown bool +} + +//lint:file-ignore U1000 Var currentDPI is used in other files, but not here +var ( + currentOrientation size.Orientation + currentDPI float32 +) + +// Declare conformity with Device +var _ fyne.Device = (*device)(nil) + +func (*device) Locale() fyne.Locale { + return lang.SystemLocale() +} + +func (*device) Orientation() fyne.DeviceOrientation { + switch currentOrientation { + case size.OrientationLandscape: + return fyne.OrientationHorizontalLeft + default: + return fyne.OrientationVertical + } +} + +func (*device) IsMobile() bool { + return true +} + +func (*device) IsBrowser() bool { + return false +} + +func (*device) HasKeyboard() bool { + return false +} + +func (d *device) ShowVirtualKeyboard() { + d.showVirtualKeyboard(mobile.DefaultKeyboard) +} + +func (d *device) ShowVirtualKeyboardType(keyboard mobile.KeyboardType) { + d.showVirtualKeyboard(keyboard) +} + +func (d *device) HideVirtualKeyboard() { + d.hideVirtualKeyboard() +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/device_android.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/device_android.go new file mode 100644 index 0000000..44c0398 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/device_android.go @@ -0,0 +1,20 @@ +//go:build android + +package mobile + +import "fyne.io/fyne/v2" + +const tapYOffset = -8.0 // to compensate for how we hold our fingers on the device + +func (*device) SystemScaleForWindow(_ fyne.Window) float32 { + if currentDPI >= 600 { + return 4 + } else if currentDPI >= 405 { + return 3 + } else if currentDPI >= 270 { + return 2 + } else if currentDPI >= 180 { + return 1.5 + } + return 1 +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/device_desktop.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/device_desktop.go new file mode 100644 index 0000000..2b51209 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/device_desktop.go @@ -0,0 +1,15 @@ +//go:build !ios && !android && !wayland + +package mobile + +import "fyne.io/fyne/v2" + +const tapYOffset = 0 // no finger compensation on desktop (simulation) + +func (*device) SystemScaleForWindow(_ fyne.Window) float32 { + return 2 // this is simply due to the high number of pixels on a mobile device - just an approximation +} + +func setDisableScreenBlank(_ bool) { + // ignore in mobile simulation mode +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/device_ios.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/device_ios.go new file mode 100644 index 0000000..284b6c0 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/device_ios.go @@ -0,0 +1,16 @@ +//go:build ios + +package mobile + +import "fyne.io/fyne/v2" + +const tapYOffset = -8.0 // to compensate for how we hold our fingers on the device + +func (*device) SystemScaleForWindow(_ fyne.Window) float32 { + if currentDPI >= 450 { + return 3 + } else if currentDPI >= 340 { + return 2.5 + } + return 2 +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/device_wayland.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/device_wayland.go new file mode 100644 index 0000000..8ad3e3a --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/device_wayland.go @@ -0,0 +1,15 @@ +//go:build wayland + +package mobile + +import "fyne.io/fyne/v2" + +const tapYOffset = -4.0 // to compensate for how we hold our fingers on the device + +func (*device) SystemScaleForWindow(_ fyne.Window) float32 { + return 1 // PinePhone simplification, our only wayland mobile currently +} + +func setDisableScreenBlank(_ bool) { + // ignore in mobile simulation mode +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/driver.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/driver.go new file mode 100644 index 0000000..984ae33 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/driver.go @@ -0,0 +1,670 @@ +package mobile + +import ( + "math" + "runtime" + "strconv" + "time" + + "fyne.io/fyne/v2" + fynecanvas "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/driver/mobile" + "fyne.io/fyne/v2/internal" + "fyne.io/fyne/v2/internal/animation" + intapp "fyne.io/fyne/v2/internal/app" + "fyne.io/fyne/v2/internal/async" + "fyne.io/fyne/v2/internal/build" + "fyne.io/fyne/v2/internal/cache" + intdriver "fyne.io/fyne/v2/internal/driver" + "fyne.io/fyne/v2/internal/driver/common" + "fyne.io/fyne/v2/internal/driver/mobile/app" + "fyne.io/fyne/v2/internal/driver/mobile/event/key" + "fyne.io/fyne/v2/internal/driver/mobile/event/lifecycle" + "fyne.io/fyne/v2/internal/driver/mobile/event/paint" + "fyne.io/fyne/v2/internal/driver/mobile/event/size" + "fyne.io/fyne/v2/internal/driver/mobile/event/touch" + "fyne.io/fyne/v2/internal/driver/mobile/gl" + "fyne.io/fyne/v2/internal/painter" + pgl "fyne.io/fyne/v2/internal/painter/gl" + "fyne.io/fyne/v2/internal/scale" + "fyne.io/fyne/v2/theme" +) + +const ( + tapMoveDecay = 0.92 // how much should the scroll continue decay on each frame? + tapMoveEndThreshold = 2.0 // at what offset will we stop decaying? + tapMoveThreshold = 4.0 // how far can we move before it is a drag + tapSecondaryDelay = 300 * time.Millisecond // how long before secondary tap + tapDoubleDelay = 500 * time.Millisecond // max duration between taps for a DoubleTap event +) + +// Configuration is the system information about the current device +type Configuration struct { + SystemTheme fyne.ThemeVariant +} + +// ConfiguredDriver is a simple type that allows packages to hook into configuration changes of this driver. +type ConfiguredDriver interface { + SetOnConfigurationChanged(func(*Configuration)) +} + +type driver struct { + app app.App + glctx gl.Context + + windows []fyne.Window + device device + animation animation.Runner + currentSize size.Event + + theme fyne.ThemeVariant + onConfigChanged func(*Configuration) + painting bool + running bool + queuedFuncs *async.UnboundedChan[func()] +} + +// Declare conformity with Driver +var ( + _ fyne.Driver = (*driver)(nil) + _ ConfiguredDriver = (*driver)(nil) +) + +func init() { + runtime.LockOSThread() +} + +func (d *driver) DoFromGoroutine(fn func(), wait bool) { + caller := func() { + if d.queuedFuncs == nil { + fn() // before the app actually starts + return + } + var done chan struct{} + if wait { + done = common.DonePool.Get() + defer common.DonePool.Put(done) + } + + d.queuedFuncs.In() <- func() { + fn() + if wait { + done <- struct{}{} + } + } + + if wait { + <-done + } + } + + if wait { + async.EnsureNotMain(caller) + } else { + caller() + } +} + +func (d *driver) CreateWindow(title string) fyne.Window { + c := newCanvas(fyne.CurrentDevice()).(*canvas) // silence lint + ret := &window{title: title, canvas: c, isChild: len(d.windows) > 0} + c.setContent(&fynecanvas.Rectangle{FillColor: theme.Color(theme.ColorNameBackground)}) + c.SetPainter(pgl.NewPainter(c, ret)) + d.windows = append(d.windows, ret) + return ret +} + +func (d *driver) AllWindows() []fyne.Window { + return d.windows +} + +// currentWindow returns the most recently opened window - we can only show one at a time. +func (d *driver) currentWindow() *window { + if len(d.windows) == 0 { + return nil + } + + var last *window + for i := len(d.windows) - 1; i >= 0; i-- { + last = d.windows[i].(*window) + if last.visible { + return last + } + } + + return last +} + +func (d *driver) Clipboard() fyne.Clipboard { + return NewClipboard() +} + +func (d *driver) RenderedTextSize(text string, textSize float32, style fyne.TextStyle, source fyne.Resource) (size fyne.Size, baseline float32) { + return painter.RenderedTextSize(text, textSize, style, source) +} + +func (d *driver) CanvasForObject(obj fyne.CanvasObject) fyne.Canvas { + if len(d.windows) == 0 { + return nil + } + + // TODO figure out how we handle multiple windows... + return d.currentWindow().Canvas() +} + +func (d *driver) AbsolutePositionForObject(co fyne.CanvasObject) fyne.Position { + c := d.CanvasForObject(co) + if c == nil { + return fyne.NewPos(0, 0) + } + + mc := c.(*canvas) + pos := intdriver.AbsolutePositionForObject(co, mc.ObjectTrees()) + inset, _ := c.InteractiveArea() + return pos.Subtract(inset) +} + +func (d *driver) GoBack() { + app.GoBack() +} + +func (d *driver) Quit() { + // Android and iOS guidelines say this should not be allowed! +} + +func (d *driver) Run() { + if d.running { + return // Run was called twice. + } + d.running = true + + app.Main(func(a app.App) { + async.SetMainGoroutine() + d.app = a + d.queuedFuncs = async.NewUnboundedChan[func()]() + + fyne.CurrentApp().Settings().AddListener(func(s fyne.Settings) { + painter.ClearFontCache() + cache.ResetThemeCaches() + intapp.ApplySettingsWithCallback(s, fyne.CurrentApp(), func(w fyne.Window) { + c, ok := w.Canvas().(*canvas) + if !ok { + return + } + c.applyThemeOutOfTreeObjects() + }) + }) + + draw := time.NewTicker(time.Second / 60) + defer func() { + l := fyne.CurrentApp().Lifecycle().(*intapp.Lifecycle) + + // exhaust the event queue + go func() { + l.WaitForEvents() + d.queuedFuncs.Close() + }() + for fn := range d.queuedFuncs.Out() { + fn() + } + + l.DestroyEventQueue() + }() + + for { + select { + case <-draw.C: + d.sendPaintEvent() + case fn := <-d.queuedFuncs.Out(): + fn() + case e, ok := <-a.Events(): + if !ok { + return // events channel closed, app done + } + current := d.currentWindow() + if current == nil { + continue + } + c := current.Canvas().(*canvas) + + switch e := a.Filter(e).(type) { + case lifecycle.Event: + d.handleLifecycle(e, current) + case size.Event: + if e.WidthPx <= 0 { + continue + } + d.currentSize = e + currentOrientation = e.Orientation + currentDPI = e.PixelsPerPt * 72 + d.setTheme(e.DarkMode) + + dev := &d.device + insetChange := dev.safeTop != e.InsetTopPx || dev.safeBottom != e.InsetBottomPx || + dev.safeLeft != e.InsetLeftPx || dev.safeRight != e.InsetRightPx + dev.safeTop = e.InsetTopPx + dev.safeLeft = e.InsetLeftPx + dev.safeBottom = e.InsetBottomPx + dev.safeRight = e.InsetRightPx + c.scale = fyne.CurrentDevice().SystemScaleForWindow(nil) + c.Painter().SetFrameBufferScale(1.0) + + if insetChange { + current.canvas.sizeContent(current.canvas.size) // even if size didn't change we invalidate + } + // make sure that we paint on the next frame + c.Content().Refresh() + case paint.Event: + d.handlePaint(e, current) + case touch.Event: + switch e.Type { + case touch.TypeBegin: + d.tapDownCanvas(current, e.X, e.Y, e.Sequence) + case touch.TypeMove: + d.tapMoveCanvas(current, e.X, e.Y, e.Sequence) + case touch.TypeEnd: + d.tapUpCanvas(current, e.X, e.Y, e.Sequence) + } + case key.Event: + if runtime.GOOS == "android" && e.Code == key.CodeDeleteBackspace && e.Rune < 0 && d.device.keyboardShown { + break // we are getting release/press on backspace during soft backspace + } + + if e.Direction == key.DirPress { + d.typeDownCanvas(c, e.Rune, e.Code, e.Modifiers) + } else if e.Direction == key.DirRelease { + d.typeUpCanvas(c, e.Rune, e.Code, e.Modifiers) + } + } + } + } + }) +} + +func (*driver) SetDisableScreenBlanking(disable bool) { + setDisableScreenBlank(disable) +} + +func (d *driver) handleLifecycle(e lifecycle.Event, w *window) { + c := w.Canvas().(*canvas) + switch e.Crosses(lifecycle.StageAlive) { + case lifecycle.CrossOn: + d.onStart() + case lifecycle.CrossOff: + d.onStop() + } + switch e.Crosses(lifecycle.StageVisible) { + case lifecycle.CrossOn: + d.glctx, _ = e.DrawContext.(gl.Context) + // this is a fix for some android phone to prevent the app from being drawn as a blank screen after being pushed in the background + c.Content().Refresh() + + d.sendPaintEvent() + case lifecycle.CrossOff: + d.glctx = nil + } + switch e.Crosses(lifecycle.StageFocused) { + case lifecycle.CrossOn: // foregrounding + if f := fyne.CurrentApp().Lifecycle().(*intapp.Lifecycle).OnEnteredForeground(); f != nil { + f() + } + case lifecycle.CrossOff: // will enter background + if runtime.GOOS == "darwin" || runtime.GOOS == "ios" { + if d.glctx == nil { + return + } + + s := fyne.NewSize(float32(d.currentSize.WidthPx)/c.scale, float32(d.currentSize.HeightPx)/c.scale) + d.paintWindow(w, s) + d.app.Publish() + } + if f := fyne.CurrentApp().Lifecycle().(*intapp.Lifecycle).OnExitedForeground(); f != nil { + f() + } + } +} + +func (d *driver) handlePaint(e paint.Event, w *window) { + c := w.Canvas().(*canvas) + if e.Window != 0 { // not all paint events come from hardware + w.handle = e.Window + } + d.painting = false + if d.glctx == nil || e.External { + return + } + if !c.initialized { + c.initialized = true + c.Painter().Init() // we cannot init until the context is set above + } + + d.animation.TickAnimations() + canvasNeedRefresh := c.FreeDirtyTextures() > 0 || c.CheckDirtyAndClear() + if canvasNeedRefresh { + newSize := fyne.NewSize(float32(d.currentSize.WidthPx)/c.scale, float32(d.currentSize.HeightPx)/c.scale) + + if c.EnsureMinSize() { + c.sizeContent(newSize) // force resize of content + } else { // if screen changed + w.Resize(newSize) + } + + d.paintWindow(w, newSize) + d.app.Publish() + } + cache.Clean(canvasNeedRefresh) +} + +func (d *driver) onStart() { + if f := fyne.CurrentApp().Lifecycle().(*intapp.Lifecycle).OnStarted(); f != nil { + f() + } +} + +func (d *driver) onStop() { + l := fyne.CurrentApp().Lifecycle().(*intapp.Lifecycle) + if f := l.OnStopped(); f != nil { + l.QueueEvent(f) + } +} + +func (d *driver) paintWindow(window fyne.Window, size fyne.Size) { + clips := &internal.ClipStack{} + c := window.Canvas().(*canvas) + + r, g, b, a := theme.Color(theme.ColorNameBackground).RGBA() + max16bit := float32(255 * 255) + d.glctx.ClearColor(float32(r)/max16bit, float32(g)/max16bit, float32(b)/max16bit, float32(a)/max16bit) + d.glctx.Clear(gl.ColorBufferBit) + + draw := func(node *common.RenderCacheNode, pos fyne.Position) { + obj := node.Obj() + if intdriver.IsClip(obj) { + inner := clips.Push(pos, obj.Size()) + c.Painter().StartClipping(inner.Rect()) + } + + if size.Width <= 0 || size.Height <= 0 { // iconifying on Windows can do bad things + return + } + c.Painter().Paint(obj, pos, size) + } + afterDraw := func(node *common.RenderCacheNode, pos fyne.Position) { + if intdriver.IsClip(node.Obj()) { + c.Painter().StopClipping() + clips.Pop() + if top := clips.Top(); top != nil { + c.Painter().StartClipping(top.Rect()) + } + } + + if build.Mode == fyne.BuildDebug { + c.DrawDebugOverlay(node.Obj(), pos, size) + } + } + + c.WalkTrees(draw, afterDraw) +} + +func (d *driver) sendPaintEvent() { + if d.painting { + return + } + d.app.Send(paint.Event{}) + d.painting = true +} + +func (d *driver) setTheme(dark bool) { + var mode fyne.ThemeVariant + if dark { + mode = theme.VariantDark + } else { + mode = theme.VariantLight + } + + if d.theme != mode && d.onConfigChanged != nil { + d.onConfigChanged(&Configuration{SystemTheme: mode}) + } + d.theme = mode +} + +func (d *driver) tapDownCanvas(w *window, x, y float32, tapID touch.Sequence) { + tapX := scale.ToFyneCoordinate(w.canvas, int(x)) + tapY := scale.ToFyneCoordinate(w.canvas, int(y)) + pos := fyne.NewPos(tapX, tapY+tapYOffset) + + w.canvas.tapDown(pos, int(tapID)) +} + +func (d *driver) tapMoveCanvas(w *window, x, y float32, tapID touch.Sequence) { + tapX := scale.ToFyneCoordinate(w.canvas, int(x)) + tapY := scale.ToFyneCoordinate(w.canvas, int(y)) + pos := fyne.NewPos(tapX, tapY+tapYOffset) + + w.canvas.tapMove(pos, int(tapID), func(wid fyne.Draggable, ev *fyne.DragEvent) { + wid.Dragged(ev) + }) +} + +func (d *driver) tapUpCanvas(w *window, x, y float32, tapID touch.Sequence) { + tapX := scale.ToFyneCoordinate(w.canvas, int(x)) + tapY := scale.ToFyneCoordinate(w.canvas, int(y)) + pos := fyne.NewPos(tapX, tapY+tapYOffset) + + w.canvas.tapUp(pos, int(tapID), func(wid fyne.Tappable, ev *fyne.PointEvent) { + wid.Tapped(ev) + }, func(wid fyne.SecondaryTappable, ev *fyne.PointEvent) { + wid.TappedSecondary(ev) + }, func(wid fyne.DoubleTappable, ev *fyne.PointEvent) { + wid.DoubleTapped(ev) + }, func(wid fyne.Draggable, ev *fyne.DragEvent) { + if math.Abs(float64(ev.Dragged.DX)) <= tapMoveEndThreshold && math.Abs(float64(ev.Dragged.DY)) <= tapMoveEndThreshold { + wid.DragEnd() + return + } + + go func() { + for math.Abs(float64(ev.Dragged.DX)) > tapMoveEndThreshold || math.Abs(float64(ev.Dragged.DY)) > tapMoveEndThreshold { + if math.Abs(float64(ev.Dragged.DX)) > 0 { + ev.Dragged.DX *= tapMoveDecay + } + if math.Abs(float64(ev.Dragged.DY)) > 0 { + ev.Dragged.DY *= tapMoveDecay + } + + d.DoFromGoroutine(func() { + wid.Dragged(ev) + }, false) + time.Sleep(time.Millisecond * 16) + } + + d.DoFromGoroutine(wid.DragEnd, false) + }() + }) +} + +var keyCodeMap = map[key.Code]fyne.KeyName{ + // non-printable + key.CodeEscape: fyne.KeyEscape, + key.CodeReturnEnter: fyne.KeyReturn, + key.CodeTab: fyne.KeyTab, + key.CodeDeleteBackspace: fyne.KeyBackspace, + key.CodeInsert: fyne.KeyInsert, + key.CodePageUp: fyne.KeyPageUp, + key.CodePageDown: fyne.KeyPageDown, + key.CodeHome: fyne.KeyHome, + key.CodeEnd: fyne.KeyEnd, + + key.CodeLeftArrow: fyne.KeyLeft, + key.CodeRightArrow: fyne.KeyRight, + key.CodeUpArrow: fyne.KeyUp, + key.CodeDownArrow: fyne.KeyDown, + + key.CodeF1: fyne.KeyF1, + key.CodeF2: fyne.KeyF2, + key.CodeF3: fyne.KeyF3, + key.CodeF4: fyne.KeyF4, + key.CodeF5: fyne.KeyF5, + key.CodeF6: fyne.KeyF6, + key.CodeF7: fyne.KeyF7, + key.CodeF8: fyne.KeyF8, + key.CodeF9: fyne.KeyF9, + key.CodeF10: fyne.KeyF10, + key.CodeF11: fyne.KeyF11, + key.CodeF12: fyne.KeyF12, + + key.CodeKeypadEnter: fyne.KeyEnter, + + // printable + key.CodeA: fyne.KeyA, + key.CodeB: fyne.KeyB, + key.CodeC: fyne.KeyC, + key.CodeD: fyne.KeyD, + key.CodeE: fyne.KeyE, + key.CodeF: fyne.KeyF, + key.CodeG: fyne.KeyG, + key.CodeH: fyne.KeyH, + key.CodeI: fyne.KeyI, + key.CodeJ: fyne.KeyJ, + key.CodeK: fyne.KeyK, + key.CodeL: fyne.KeyL, + key.CodeM: fyne.KeyM, + key.CodeN: fyne.KeyN, + key.CodeO: fyne.KeyO, + key.CodeP: fyne.KeyP, + key.CodeQ: fyne.KeyQ, + key.CodeR: fyne.KeyR, + key.CodeS: fyne.KeyS, + key.CodeT: fyne.KeyT, + key.CodeU: fyne.KeyU, + key.CodeV: fyne.KeyV, + key.CodeW: fyne.KeyW, + key.CodeX: fyne.KeyX, + key.CodeY: fyne.KeyY, + key.CodeZ: fyne.KeyZ, + key.Code0: fyne.Key0, + key.CodeKeypad0: fyne.Key0, + key.Code1: fyne.Key1, + key.CodeKeypad1: fyne.Key1, + key.Code2: fyne.Key2, + key.CodeKeypad2: fyne.Key2, + key.Code3: fyne.Key3, + key.CodeKeypad3: fyne.Key3, + key.Code4: fyne.Key4, + key.CodeKeypad4: fyne.Key4, + key.Code5: fyne.Key5, + key.CodeKeypad5: fyne.Key5, + key.Code6: fyne.Key6, + key.CodeKeypad6: fyne.Key6, + key.Code7: fyne.Key7, + key.CodeKeypad7: fyne.Key7, + key.Code8: fyne.Key8, + key.CodeKeypad8: fyne.Key8, + key.Code9: fyne.Key9, + key.CodeKeypad9: fyne.Key9, + + key.CodeSemicolon: fyne.KeySemicolon, + key.CodeEqualSign: fyne.KeyEqual, + + key.CodeSpacebar: fyne.KeySpace, + key.CodeApostrophe: fyne.KeyApostrophe, + key.CodeComma: fyne.KeyComma, + key.CodeHyphenMinus: fyne.KeyMinus, + key.CodeKeypadHyphenMinus: fyne.KeyMinus, + key.CodeFullStop: fyne.KeyPeriod, + key.CodeKeypadFullStop: fyne.KeyPeriod, + key.CodeSlash: fyne.KeySlash, + key.CodeLeftSquareBracket: fyne.KeyLeftBracket, + key.CodeBackslash: fyne.KeyBackslash, + key.CodeRightSquareBracket: fyne.KeyRightBracket, + key.CodeGraveAccent: fyne.KeyBackTick, + + key.CodeBackButton: mobile.KeyBack, +} + +func keyToName(code key.Code) fyne.KeyName { + ret, ok := keyCodeMap[code] + if !ok { + return "" + } + + return ret +} + +func runeToPrintable(r rune) rune { + if strconv.IsPrint(r) { + return r + } + + return 0 +} + +func (d *driver) typeDownCanvas(canvas *canvas, r rune, code key.Code, mod key.Modifiers) { + keyName := keyToName(code) + switch keyName { + case fyne.KeyTab: + capture := false + if ent, ok := canvas.Focused().(fyne.Tabbable); ok { + capture = ent.AcceptsTab() + } + if !capture { + switch mod { + case 0: + canvas.FocusNext() + return + case key.ModShift: + canvas.FocusPrevious() + return + } + } + } + + r = runeToPrintable(r) + keyEvent := &fyne.KeyEvent{Name: keyName} + + if canvas.Focused() != nil { + if keyName != "" { + canvas.Focused().TypedKey(keyEvent) + } + if r > 0 { + canvas.Focused().TypedRune(r) + } + } else { + if keyName != "" { + if canvas.onTypedKey != nil { + canvas.onTypedKey(keyEvent) + } else if keyName == mobile.KeyBack { + d.GoBack() + } + } + if r > 0 && canvas.onTypedRune != nil { + canvas.onTypedRune(r) + } + } +} + +func (d *driver) typeUpCanvas(_ *canvas, _ rune, _ key.Code, _ key.Modifiers) { +} + +func (d *driver) Device() fyne.Device { + return &d.device +} + +func (d *driver) SetOnConfigurationChanged(f func(*Configuration)) { + d.onConfigChanged = f +} + +func (d *driver) DoubleTapDelay() time.Duration { + return tapDoubleDelay +} + +// NewGoMobileDriver sets up a new Driver instance implemented using the Go +// Mobile extension and OpenGL bindings. +func NewGoMobileDriver() fyne.Driver { + d := &driver{ + theme: fyne.ThemeVariant(2), // unspecified + } + + registerRepository(d) + return d +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/driver_android.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/driver_android.go new file mode 100644 index 0000000..da28d4c --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/driver_android.go @@ -0,0 +1,23 @@ +//go:build android + +package mobile + +import driverDefs "fyne.io/fyne/v2/driver" + +/* +#include +#include + +void keepScreenOn(uintptr_t jni_env, uintptr_t ctx, bool disabled); +*/ +import "C" + +func setDisableScreenBlank(disable bool) { + driverDefs.RunNative(func(ctx any) error { + ac := ctx.(*driverDefs.AndroidContext) + + C.keepScreenOn(C.uintptr_t(ac.Env), C.uintptr_t(ac.Ctx), C.bool(disable)) + + return nil + }) +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/driver_ios.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/driver_ios.go new file mode 100644 index 0000000..0affb73 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/driver_ios.go @@ -0,0 +1,15 @@ +//go:build ios + +package mobile + +/* +#cgo darwin LDFLAGS: -framework UIKit +#import + +void disableIdleTimer(BOOL disabled); +*/ +import "C" + +func setDisableScreenBlank(disable bool) { + C.disableIdleTimer(C.BOOL(disable)) +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/driver_ios.m b/vendor/fyne.io/fyne/v2/internal/driver/mobile/driver_ios.m new file mode 100644 index 0000000..33c2916 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/driver_ios.m @@ -0,0 +1,12 @@ +//go:build ios + +#import +#import + +void disableIdleTimer(BOOL disabled) { + @autoreleasepool { + [[NSOperationQueue mainQueue] addOperationWithBlock:^ { + [UIApplication sharedApplication].idleTimerDisabled = disabled; + }]; + } +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/event/key/code_string.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/event/key/code_string.go new file mode 100644 index 0000000..63a6c74 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/event/key/code_string.go @@ -0,0 +1,183 @@ +// Code generated by "stringer -type=Code"; DO NOT EDIT. + +package key + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[CodeUnknown-0] + _ = x[CodeA-4] + _ = x[CodeB-5] + _ = x[CodeC-6] + _ = x[CodeD-7] + _ = x[CodeE-8] + _ = x[CodeF-9] + _ = x[CodeG-10] + _ = x[CodeH-11] + _ = x[CodeI-12] + _ = x[CodeJ-13] + _ = x[CodeK-14] + _ = x[CodeL-15] + _ = x[CodeM-16] + _ = x[CodeN-17] + _ = x[CodeO-18] + _ = x[CodeP-19] + _ = x[CodeQ-20] + _ = x[CodeR-21] + _ = x[CodeS-22] + _ = x[CodeT-23] + _ = x[CodeU-24] + _ = x[CodeV-25] + _ = x[CodeW-26] + _ = x[CodeX-27] + _ = x[CodeY-28] + _ = x[CodeZ-29] + _ = x[Code1-30] + _ = x[Code2-31] + _ = x[Code3-32] + _ = x[Code4-33] + _ = x[Code5-34] + _ = x[Code6-35] + _ = x[Code7-36] + _ = x[Code8-37] + _ = x[Code9-38] + _ = x[Code0-39] + _ = x[CodeReturnEnter-40] + _ = x[CodeEscape-41] + _ = x[CodeDeleteBackspace-42] + _ = x[CodeTab-43] + _ = x[CodeSpacebar-44] + _ = x[CodeHyphenMinus-45] + _ = x[CodeEqualSign-46] + _ = x[CodeLeftSquareBracket-47] + _ = x[CodeRightSquareBracket-48] + _ = x[CodeBackslash-49] + _ = x[CodeSemicolon-51] + _ = x[CodeApostrophe-52] + _ = x[CodeGraveAccent-53] + _ = x[CodeComma-54] + _ = x[CodeFullStop-55] + _ = x[CodeSlash-56] + _ = x[CodeCapsLock-57] + _ = x[CodeF1-58] + _ = x[CodeF2-59] + _ = x[CodeF3-60] + _ = x[CodeF4-61] + _ = x[CodeF5-62] + _ = x[CodeF6-63] + _ = x[CodeF7-64] + _ = x[CodeF8-65] + _ = x[CodeF9-66] + _ = x[CodeF10-67] + _ = x[CodeF11-68] + _ = x[CodeF12-69] + _ = x[CodePause-72] + _ = x[CodeInsert-73] + _ = x[CodeHome-74] + _ = x[CodePageUp-75] + _ = x[CodeDeleteForward-76] + _ = x[CodeEnd-77] + _ = x[CodePageDown-78] + _ = x[CodeRightArrow-79] + _ = x[CodeLeftArrow-80] + _ = x[CodeDownArrow-81] + _ = x[CodeUpArrow-82] + _ = x[CodeKeypadNumLock-83] + _ = x[CodeKeypadSlash-84] + _ = x[CodeKeypadAsterisk-85] + _ = x[CodeKeypadHyphenMinus-86] + _ = x[CodeKeypadPlusSign-87] + _ = x[CodeKeypadEnter-88] + _ = x[CodeKeypad1-89] + _ = x[CodeKeypad2-90] + _ = x[CodeKeypad3-91] + _ = x[CodeKeypad4-92] + _ = x[CodeKeypad5-93] + _ = x[CodeKeypad6-94] + _ = x[CodeKeypad7-95] + _ = x[CodeKeypad8-96] + _ = x[CodeKeypad9-97] + _ = x[CodeKeypad0-98] + _ = x[CodeKeypadFullStop-99] + _ = x[CodeKeypadEqualSign-103] + _ = x[CodeF13-104] + _ = x[CodeF14-105] + _ = x[CodeF15-106] + _ = x[CodeF16-107] + _ = x[CodeF17-108] + _ = x[CodeF18-109] + _ = x[CodeF19-110] + _ = x[CodeF20-111] + _ = x[CodeF21-112] + _ = x[CodeF22-113] + _ = x[CodeF23-114] + _ = x[CodeF24-115] + _ = x[CodeHelp-117] + _ = x[CodeMute-127] + _ = x[CodeVolumeUp-128] + _ = x[CodeVolumeDown-129] + _ = x[CodeLeftControl-224] + _ = x[CodeLeftShift-225] + _ = x[CodeLeftAlt-226] + _ = x[CodeLeftGUI-227] + _ = x[CodeRightControl-228] + _ = x[CodeRightShift-229] + _ = x[CodeRightAlt-230] + _ = x[CodeRightGUI-231] + _ = x[CodeCompose-65536] +} + +const ( + _Code_name_0 = "CodeUnknown" + _Code_name_1 = "CodeACodeBCodeCCodeDCodeECodeFCodeGCodeHCodeICodeJCodeKCodeLCodeMCodeNCodeOCodePCodeQCodeRCodeSCodeTCodeUCodeVCodeWCodeXCodeYCodeZCode1Code2Code3Code4Code5Code6Code7Code8Code9Code0CodeReturnEnterCodeEscapeCodeDeleteBackspaceCodeTabCodeSpacebarCodeHyphenMinusCodeEqualSignCodeLeftSquareBracketCodeRightSquareBracketCodeBackslash" + _Code_name_2 = "CodeSemicolonCodeApostropheCodeGraveAccentCodeCommaCodeFullStopCodeSlashCodeCapsLockCodeF1CodeF2CodeF3CodeF4CodeF5CodeF6CodeF7CodeF8CodeF9CodeF10CodeF11CodeF12" + _Code_name_3 = "CodePauseCodeInsertCodeHomeCodePageUpCodeDeleteForwardCodeEndCodePageDownCodeRightArrowCodeLeftArrowCodeDownArrowCodeUpArrowCodeKeypadNumLockCodeKeypadSlashCodeKeypadAsteriskCodeKeypadHyphenMinusCodeKeypadPlusSignCodeKeypadEnterCodeKeypad1CodeKeypad2CodeKeypad3CodeKeypad4CodeKeypad5CodeKeypad6CodeKeypad7CodeKeypad8CodeKeypad9CodeKeypad0CodeKeypadFullStop" + _Code_name_4 = "CodeKeypadEqualSignCodeF13CodeF14CodeF15CodeF16CodeF17CodeF18CodeF19CodeF20CodeF21CodeF22CodeF23CodeF24" + _Code_name_5 = "CodeHelp" + _Code_name_6 = "CodeMuteCodeVolumeUpCodeVolumeDown" + _Code_name_7 = "CodeLeftControlCodeLeftShiftCodeLeftAltCodeLeftGUICodeRightControlCodeRightShiftCodeRightAltCodeRightGUI" + _Code_name_8 = "CodeCompose" +) + +var ( + _Code_index_1 = [...]uint16{0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100, 105, 110, 115, 120, 125, 130, 135, 140, 145, 150, 155, 160, 165, 170, 175, 180, 195, 205, 224, 231, 243, 258, 271, 292, 314, 327} + _Code_index_2 = [...]uint8{0, 13, 27, 42, 51, 63, 72, 84, 90, 96, 102, 108, 114, 120, 126, 132, 138, 145, 152, 159} + _Code_index_3 = [...]uint16{0, 9, 19, 27, 37, 54, 61, 73, 87, 100, 113, 124, 141, 156, 174, 195, 213, 228, 239, 250, 261, 272, 283, 294, 305, 316, 327, 338, 356} + _Code_index_4 = [...]uint8{0, 19, 26, 33, 40, 47, 54, 61, 68, 75, 82, 89, 96, 103} + _Code_index_6 = [...]uint8{0, 8, 20, 34} + _Code_index_7 = [...]uint8{0, 15, 28, 39, 50, 66, 80, 92, 104} +) + +func (i Code) String() string { + switch { + case i == 0: + return _Code_name_0 + case 4 <= i && i <= 49: + i -= 4 + return _Code_name_1[_Code_index_1[i]:_Code_index_1[i+1]] + case 51 <= i && i <= 69: + i -= 51 + return _Code_name_2[_Code_index_2[i]:_Code_index_2[i+1]] + case 72 <= i && i <= 99: + i -= 72 + return _Code_name_3[_Code_index_3[i]:_Code_index_3[i+1]] + case 103 <= i && i <= 115: + i -= 103 + return _Code_name_4[_Code_index_4[i]:_Code_index_4[i+1]] + case i == 117: + return _Code_name_5 + case 127 <= i && i <= 129: + i -= 127 + return _Code_name_6[_Code_index_6[i]:_Code_index_6[i+1]] + case 224 <= i && i <= 231: + i -= 224 + return _Code_name_7[_Code_index_7[i]:_Code_index_7[i+1]] + case i == 65536: + return _Code_name_8 + default: + return "Code(" + strconv.FormatInt(int64(i), 10) + ")" + } +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/event/key/key.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/event/key/key.go new file mode 100644 index 0000000..2b20e3e --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/event/key/key.go @@ -0,0 +1,274 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:generate stringer -type=Code + +// Package key defines an event for physical keyboard keys. +// +// On-screen software keyboards do not send key events. +// +// See the golang.org/x/mobile/app package for details on the event model. +package key + +import ( + "fmt" + "strings" +) + +// Event is a key event. +type Event struct { + // Rune is the meaning of the key event as determined by the + // operating system. The mapping is determined by system-dependent + // current layout, modifiers, lock-states, etc. + // + // If non-negative, it is a Unicode codepoint: pressing the 'a' key + // generates different Runes 'a' or 'A' (but the same Code) depending on + // the state of the shift key. + // + // If -1, the key does not generate a Unicode codepoint. To distinguish + // them, look at Code. + Rune rune + + // Code is the identity of the physical key relative to a notional + // "standard" keyboard, independent of current layout, modifiers, + // lock-states, etc + // + // For standard key codes, its value matches USB HID key codes. + // Compare its value to uint32-typed constants in this package, such + // as CodeLeftShift and CodeEscape. + // + // Pressing the regular '2' key and number-pad '2' key (with Num-Lock) + // generate different Codes (but the same Rune). + Code Code + + // Modifiers is a bitmask representing a set of modifier keys: ModShift, + // ModAlt, etc. + Modifiers Modifiers + + // Direction is the direction of the key event: DirPress, DirRelease, + // or DirNone (for key repeats). + Direction Direction + + // TODO: add a Device ID, for multiple input devices? + // TODO: add a time.Time? +} + +func (e Event) String() string { + if e.Rune >= 0 { + return fmt.Sprintf("key.Event{%q (%v), %v, %v}", e.Rune, e.Code, e.Modifiers, e.Direction) + } + return fmt.Sprintf("key.Event{(%v), %v, %v}", e.Code, e.Modifiers, e.Direction) +} + +// Direction is the direction of the key event. +type Direction uint8 + +// All possibledirections of key event. +const ( + DirNone Direction = 0 + DirPress Direction = 1 + DirRelease Direction = 2 +) + +// Modifiers is a bitmask representing a set of modifier keys. +type Modifiers uint32 + +// All possible modifier keys. +const ( + ModShift Modifiers = 1 << 0 + ModControl Modifiers = 1 << 1 + ModAlt Modifiers = 1 << 2 + ModMeta Modifiers = 1 << 3 // called "Command" on OS X +) + +// Code is the identity of a key relative to a notional "standard" keyboard. +type Code uint32 + +// Physical key codes. +// +// For standard key codes, its value matches USB HID key codes. +// TODO: add missing codes. +const ( + CodeUnknown Code = 0 + + CodeA Code = 4 + CodeB Code = 5 + CodeC Code = 6 + CodeD Code = 7 + CodeE Code = 8 + CodeF Code = 9 + CodeG Code = 10 + CodeH Code = 11 + CodeI Code = 12 + CodeJ Code = 13 + CodeK Code = 14 + CodeL Code = 15 + CodeM Code = 16 + CodeN Code = 17 + CodeO Code = 18 + CodeP Code = 19 + CodeQ Code = 20 + CodeR Code = 21 + CodeS Code = 22 + CodeT Code = 23 + CodeU Code = 24 + CodeV Code = 25 + CodeW Code = 26 + CodeX Code = 27 + CodeY Code = 28 + CodeZ Code = 29 + + Code1 Code = 30 + Code2 Code = 31 + Code3 Code = 32 + Code4 Code = 33 + Code5 Code = 34 + Code6 Code = 35 + Code7 Code = 36 + Code8 Code = 37 + Code9 Code = 38 + Code0 Code = 39 + + CodeReturnEnter Code = 40 + CodeEscape Code = 41 + CodeDeleteBackspace Code = 42 + CodeTab Code = 43 + CodeSpacebar Code = 44 + CodeHyphenMinus Code = 45 // - + CodeEqualSign Code = 46 // = + CodeLeftSquareBracket Code = 47 // [ + CodeRightSquareBracket Code = 48 // ] + CodeBackslash Code = 49 // \ + CodeSemicolon Code = 51 // ; + CodeApostrophe Code = 52 // ' + CodeGraveAccent Code = 53 // ` + CodeComma Code = 54 // , + CodeFullStop Code = 55 // . + CodeSlash Code = 56 // / + CodeCapsLock Code = 57 + + CodeF1 Code = 58 + CodeF2 Code = 59 + CodeF3 Code = 60 + CodeF4 Code = 61 + CodeF5 Code = 62 + CodeF6 Code = 63 + CodeF7 Code = 64 + CodeF8 Code = 65 + CodeF9 Code = 66 + CodeF10 Code = 67 + CodeF11 Code = 68 + CodeF12 Code = 69 + + CodePause Code = 72 + CodeInsert Code = 73 + CodeHome Code = 74 + CodePageUp Code = 75 + CodeDeleteForward Code = 76 + CodeEnd Code = 77 + CodePageDown Code = 78 + + CodeRightArrow Code = 79 + CodeLeftArrow Code = 80 + CodeDownArrow Code = 81 + CodeUpArrow Code = 82 + + CodeKeypadNumLock Code = 83 + CodeKeypadSlash Code = 84 // / + CodeKeypadAsterisk Code = 85 // * + CodeKeypadHyphenMinus Code = 86 // - + CodeKeypadPlusSign Code = 87 // + + CodeKeypadEnter Code = 88 + CodeKeypad1 Code = 89 + CodeKeypad2 Code = 90 + CodeKeypad3 Code = 91 + CodeKeypad4 Code = 92 + CodeKeypad5 Code = 93 + CodeKeypad6 Code = 94 + CodeKeypad7 Code = 95 + CodeKeypad8 Code = 96 + CodeKeypad9 Code = 97 + CodeKeypad0 Code = 98 + CodeKeypadFullStop Code = 99 // . + CodeKeypadEqualSign Code = 103 // = + + CodeF13 Code = 104 + CodeF14 Code = 105 + CodeF15 Code = 106 + CodeF16 Code = 107 + CodeF17 Code = 108 + CodeF18 Code = 109 + CodeF19 Code = 110 + CodeF20 Code = 111 + CodeF21 Code = 112 + CodeF22 Code = 113 + CodeF23 Code = 114 + CodeF24 Code = 115 + + CodeHelp Code = 117 + + CodeMute Code = 127 + CodeVolumeUp Code = 128 + CodeVolumeDown Code = 129 + + CodeLeftControl Code = 224 + CodeLeftShift Code = 225 + CodeLeftAlt Code = 226 + CodeLeftGUI Code = 227 + CodeRightControl Code = 228 + CodeRightShift Code = 229 + CodeRightAlt Code = 230 + CodeRightGUI Code = 231 + + CodeBackButton Code = 301 // anything above 255 is not used in the USB spec + + // The following codes are not part of the standard USB HID Usage IDs for + // keyboards. See http://www.usb.org/developers/hidpage/Hut1_12v2.pdf + // + // Usage IDs are uint16s, so these non-standard values start at 0x10000. + + // CodeCompose is the Code for a compose key, sometimes called a multi key, + // used to input non-ASCII characters such as ñ being composed of n and ~. + // + // See https://en.wikipedia.org/wiki/Compose_key + CodeCompose Code = 0x10000 +) + +// TODO: Given we use runes outside the unicode space, should we provide a +// printing function? Related: it's a little unfortunate that printing a +// key.Event with %v gives not very readable output like: +// {100 7 key.Modifiers() Press} + +var mods = [...]struct { + m Modifiers + s string +}{ + {ModShift, "Shift"}, + {ModControl, "Control"}, + {ModAlt, "Alt"}, + {ModMeta, "Meta"}, +} + +func (m Modifiers) String() string { + var match []string + for _, mod := range mods { + if mod.m&m != 0 { + match = append(match, mod.s) + } + } + return "key.Modifiers(" + strings.Join(match, "|") + ")" +} + +func (d Direction) String() string { + switch d { + case DirNone: + return "None" + case DirPress: + return "Press" + case DirRelease: + return "Release" + default: + return fmt.Sprintf("key.Direction(%d)", d) + } +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/event/lifecycle/lifecycle.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/event/lifecycle/lifecycle.go new file mode 100644 index 0000000..7bd3945 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/event/lifecycle/lifecycle.go @@ -0,0 +1,137 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package lifecycle defines an event for an app's lifecycle. +// +// The app lifecycle consists of moving back and forth between an ordered +// sequence of stages. For example, being at a stage greater than or equal to +// StageVisible means that the app is visible on the screen. +// +// A lifecycle event is a change from one stage to another, which crosses every +// intermediate stage. For example, changing from StageAlive to StageFocused +// implicitly crosses StageVisible. +// +// Crosses can be in a positive or negative direction. A positive crossing of +// StageFocused means that the app has gained the focus. A negative crossing +// means it has lost the focus. +// +// See the golang.org/x/mobile/app package for details on the event model. +package lifecycle // import "fyne.io/fyne/v2/internal/driver/mobile/event/lifecycle" + +import ( + "fmt" +) + +// Cross is whether a lifecycle stage was crossed. +type Cross uint32 + +func (c Cross) String() string { + switch c { + case CrossOn: + return "on" + case CrossOff: + return "off" + } + return "none" +} + +// All possible cross of a lifecycle. +const ( + CrossNone Cross = 0 + CrossOn Cross = 1 + CrossOff Cross = 2 +) + +// Event is a lifecycle change from an old stage to a new stage. +type Event struct { + From, To Stage + + // DrawContext is the state used for painting, if any is valid. + // + // For OpenGL apps, a non-nil DrawContext is a gl.Context. + // + // TODO: make this an App method if we move away from an event channel? + DrawContext any +} + +func (e Event) String() string { + return fmt.Sprintf("lifecycle.Event{From:%v, To:%v, DrawContext:%v}", e.From, e.To, e.DrawContext) +} + +// Crosses reports whether the transition from From to To crosses the stage s: +// - It returns CrossOn if it does, and the lifecycle change is positive. +// - It returns CrossOff if it does, and the lifecycle change is negative. +// - Otherwise, it returns CrossNone. +// +// See the documentation for Stage for more discussion of positive and negative +// crosses. +func (e Event) Crosses(s Stage) Cross { + switch { + case e.From < s && e.To >= s: + return CrossOn + case e.From >= s && e.To < s: + return CrossOff + } + return CrossNone +} + +// Stage is a stage in the app's lifecycle. The values are ordered, so that a +// lifecycle change from stage From to stage To implicitly crosses every stage +// in the range (min, max], exclusive on the low end and inclusive on the high +// end, where min is the minimum of From and To, and max is the maximum. +// +// The documentation for individual stages talk about positive and negative +// crosses. A positive lifecycle change is one where its From stage is less +// than its To stage. Similarly, a negative lifecycle change is one where From +// is greater than To. Thus, a positive lifecycle change crosses every stage in +// the range (From, To] in increasing order, and a negative lifecycle change +// crosses every stage in the range (To, From] in decreasing order. +type Stage uint32 + +// TODO: how does iOS map to these stages? What do cross-platform mobile +// abstractions do? + +const ( + // StageDead is the zero stage. No lifecycle change crosses this stage, + // but: + // - A positive change from this stage is the very first lifecycle change. + // - A negative change to this stage is the very last lifecycle change. + StageDead Stage = iota + + // StageAlive means that the app is alive. + // - A positive cross means that the app has been created. + // - A negative cross means that the app is being destroyed. + // Each cross, either from or to StageDead, will occur only once. + // On Android, these correspond to onCreate and onDestroy. + StageAlive + + // StageVisible means that the app window is visible. + // - A positive cross means that the app window has become visible. + // - A negative cross means that the app window has become invisible. + // On Android, these correspond to onStart and onStop. + // On Desktop, an app window can become invisible if e.g. it is minimized, + // unmapped, or not on a visible workspace. + StageVisible + + // StageFocused means that the app window has the focus. + // - A positive cross means that the app window has gained the focus. + // - A negative cross means that the app window has lost the focus. + // On Android, these correspond to onResume and onFreeze. + StageFocused +) + +func (s Stage) String() string { + switch s { + case StageDead: + return "StageDead" + case StageAlive: + return "StageAlive" + case StageVisible: + return "StageVisible" + case StageFocused: + return "StageFocused" + default: + return fmt.Sprintf("lifecycle.Stage(%d)", s) + } +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/event/paint/paint.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/event/paint/paint.go new file mode 100644 index 0000000..933b7fe --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/event/paint/paint.go @@ -0,0 +1,27 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package paint defines an event for the app being ready to paint. +// +// See the golang.org/x/mobile/app package for details on the event model. +package paint // import "fyne.io/fyne/v2/internal/driver/mobile/event/paint" + +// Event indicates that the app is ready to paint the next frame of the GUI. +// +// A frame is completed by calling the App's Publish method. +type Event struct { + // External is true for paint events sent by the screen driver. + // + // An external event may be sent at any time in response to an + // operating system event, for example the window opened, was + // resized, or the screen memory was lost. + // + // Programs actively drawing to the screen as fast as vsync allows + // should ignore external paint events to avoid a backlog of paint + // events building up. + External bool + + // Window specifies a native handle for the window being painted into. + Window uintptr +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/event/size/size.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/event/size/size.go new file mode 100644 index 0000000..3e951ee --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/event/size/size.go @@ -0,0 +1,98 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package size defines an event for the dimensions, physical resolution and +// orientation of the app's window. +// +// See the golang.org/x/mobile/app package for details on the event model. +package size // import "fyne.io/fyne/v2/internal/driver/mobile/event/size" + +import ( + "image" +) + +// Event holds the dimensions, physical resolution and orientation of the app's +// window. +type Event struct { + // WidthPx and HeightPx are the window's dimensions in pixels. + WidthPx, HeightPx int + + // WidthPt and HeightPt are the window's physical dimensions in points + // (1/72 of an inch). + // + // The values are based on PixelsPerPt and are therefore approximate, as + // per the comment on PixelsPerPt. + WidthPt, HeightPt float32 + + // PixelsPerPt is the window's physical resolution. It is the number of + // pixels in a single float32. + // + // There are a wide variety of pixel densities in existing phones and + // tablets, so apps should be written to expect various non-integer + // PixelsPerPt values. + // + // The value is approximate, in that the OS, drivers or hardware may report + // approximate or quantized values. An N x N pixel square should be roughly + // 1 square inch for N = int(PixelsPerPt * 72), although different square + // lengths (in pixels) might be closer to 1 inch in practice. Nonetheless, + // this PixelsPerPt value should be consistent with e.g. the ratio of + // WidthPx to WidthPt. + PixelsPerPt float32 + + // Orientation is the orientation of the device screen. + Orientation Orientation + + // InsetTopPx, InsetBottomPx, InsetLeftPx and InsetRightPx define the size of any border area in pixels. + // These values define how far in from the screen edge any controls should be drawn. + // The inset can be caused by status bars, button overlays or devices cutouts. + InsetTopPx, InsetBottomPx, InsetLeftPx, InsetRightPx int + + // DarkMode is set to true if this window is currently shown in the OS configured dark / night mode. + DarkMode bool +} + +// Size returns the window's size in pixels, at the time this size event was +// sent. +func (e Event) Size() image.Point { + return image.Point{e.WidthPx, e.HeightPx} +} + +// Bounds returns the window's bounds in pixels, at the time this size event +// was sent. +// +// The top-left pixel is always (0, 0). The bottom-right pixel is given by the +// width and height. +func (e Event) Bounds() image.Rectangle { + return image.Rectangle{Max: image.Point{e.WidthPx, e.HeightPx}} +} + +// Orientation is the orientation of the device screen. +type Orientation int + +const ( + // OrientationUnknown means device orientation cannot be determined. + // + // Equivalent on Android to Configuration.ORIENTATION_UNKNOWN + // and on iOS to: + // UIDeviceOrientationUnknown + // UIDeviceOrientationFaceUp + // UIDeviceOrientationFaceDown + OrientationUnknown Orientation = iota + + // OrientationPortrait is a device oriented so it is tall and thin. + // + // Equivalent on Android to Configuration.ORIENTATION_PORTRAIT + // and on iOS to: + // UIDeviceOrientationPortrait + // UIDeviceOrientationPortraitUpsideDown + OrientationPortrait + + // OrientationLandscape is a device oriented so it is short and wide. + // + // Equivalent on Android to Configuration.ORIENTATION_LANDSCAPE + // and on iOS to: + // UIDeviceOrientationLandscapeLeft + // UIDeviceOrientationLandscapeRight + OrientationLandscape +) diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/event/touch/touch.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/event/touch/touch.go new file mode 100644 index 0000000..c03e0b0 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/event/touch/touch.go @@ -0,0 +1,72 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package touch defines an event for touch input. +// +// See the golang.org/x/mobile/app package for details on the event model. +package touch // import "fyne.io/fyne/v2/internal/driver/mobile/event/touch" + +// The best source on android input events is the NDK: include/android/input.h +// +// iOS event handling guide: +// https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS + +import ( + "fmt" +) + +// Event is a touch event. +type Event struct { + // X and Y are the touch location, in pixels. + X, Y float32 + + // Sequence is the sequence number. The same number is shared by all events + // in a sequence. A sequence begins with a single TypeBegin, is followed by + // zero or more TypeMoves, and ends with a single TypeEnd. A Sequence + // distinguishes concurrent sequences but its value is subsequently reused. + Sequence Sequence + + // Type is the touch type. + Type Type +} + +// Sequence identifies a sequence of touch events. +type Sequence int64 + +// Type describes the type of a touch event. +type Type byte + +const ( + // TypeBegin is a user first touching the device. + // + // On Android, this is a AMOTION_EVENT_ACTION_DOWN. + // On iOS, this is a call to touchesBegan. + TypeBegin Type = iota + + // TypeMove is a user dragging across the device. + // + // A TypeMove is delivered between a TypeBegin and TypeEnd. + // + // On Android, this is a AMOTION_EVENT_ACTION_MOVE. + // On iOS, this is a call to touchesMoved. + TypeMove + + // TypeEnd is a user no longer touching the device. + // + // On Android, this is a AMOTION_EVENT_ACTION_UP. + // On iOS, this is a call to touchesEnded. + TypeEnd +) + +func (t Type) String() string { + switch t { + case TypeBegin: + return "begin" + case TypeMove: + return "move" + case TypeEnd: + return "end" + } + return fmt.Sprintf("touch.Type(%d)", t) +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/file.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/file.go new file mode 100644 index 0000000..6f3d2a4 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/file.go @@ -0,0 +1,137 @@ +package mobile + +import ( + "io" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/driver/mobile/app" + "fyne.io/fyne/v2/storage" +) + +type fileOpen struct { + io.ReadCloser + uri fyne.URI + done func() +} + +func (f *fileOpen) URI() fyne.URI { + return f.uri +} + +func fileReaderForURI(u fyne.URI) (fyne.URIReadCloser, error) { + file := &fileOpen{uri: u} + read, err := nativeFileOpen(file) + if read == nil { + return nil, err + } + file.ReadCloser = read + return file, err +} + +func mobileFilter(filter storage.FileFilter) *app.FileFilter { + mobile := &app.FileFilter{} + + if f, ok := filter.(*storage.MimeTypeFileFilter); ok { + mobile.MimeTypes = f.MimeTypes + } else if f, ok := filter.(*storage.ExtensionFileFilter); ok { + mobile.Extensions = f.Extensions + } else if filter != nil { + fyne.LogError("Custom filter types not supported on mobile", nil) + } + + return mobile +} + +type hasOpenPicker interface { + ShowFileOpenPicker(func(string, func()), *app.FileFilter) +} + +// ShowFileOpenPicker loads the native file open dialog and returns the chosen file path via the callback func. +func ShowFileOpenPicker(callback func(fyne.URIReadCloser, error), filter storage.FileFilter) { + drv := fyne.CurrentApp().Driver().(*driver) + if a, ok := drv.app.(hasOpenPicker); ok { + a.ShowFileOpenPicker(func(uri string, closer func()) { + if uri == "" { + callback(nil, nil) + return + } + f, err := fileReaderForURI(nativeURI(uri)) + if f != nil { + f.(*fileOpen).done = closer + } + callback(f, err) + }, mobileFilter(filter)) + } +} + +// ShowFolderOpenPicker loads the native folder open dialog and calls back the chosen directory path as a ListableURI. +func ShowFolderOpenPicker(callback func(fyne.ListableURI, error)) { + filter := storage.NewMimeTypeFileFilter([]string{"application/x-directory"}) + drv := fyne.CurrentApp().Driver().(*driver) + if a, ok := drv.app.(hasOpenPicker); ok { + a.ShowFileOpenPicker(func(path string, _ func()) { + if path == "" { + callback(nil, nil) + return + } + + uri, err := storage.ParseURI(path) + if err != nil { + callback(nil, err) + return + } + + callback(listerForURI(uri)) + }, mobileFilter(filter)) + } +} + +type fileSave struct { + io.WriteCloser + uri fyne.URI + done func() +} + +func (f *fileSave) URI() fyne.URI { + return f.uri +} + +func fileWriterForURI(u fyne.URI, truncate bool) (fyne.URIWriteCloser, error) { + file := &fileSave{uri: u} + write, err := nativeFileSave(file, truncate) + if write == nil { + return nil, err + } + file.WriteCloser = write + return file, err +} + +type hasSavePicker interface { + ShowFileSavePicker(func(string, func()), *app.FileFilter, string) +} + +// ShowFileSavePicker loads the native file save dialog and returns the chosen file path via the callback func. +func ShowFileSavePicker(callback func(fyne.URIWriteCloser, error), filter storage.FileFilter, filename string) { + drv := fyne.CurrentApp().Driver().(*driver) + if a, ok := drv.app.(hasSavePicker); ok { + a.ShowFileSavePicker(func(path string, closer func()) { + if path == "" { + callback(nil, nil) + return + } + + uri, err := storage.ParseURI(path) + if err != nil { + callback(nil, err) + return + } + + // TODO: does the save dialog want to truncate by default? + f, err := fileWriterForURI(uri, true) + if f != nil { + f.(*fileSave).done = closer + } + callback(f, err) + }, mobileFilter(filter), filename) + } +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/file_android.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/file_android.go new file mode 100644 index 0000000..095b5bf --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/file_android.go @@ -0,0 +1,180 @@ +//go:build android + +package mobile + +/* +#cgo LDFLAGS: -landroid -llog + +#include +#include + +bool deleteURI(uintptr_t jni_env, uintptr_t ctx, char* uriCstr); +bool existsURI(uintptr_t jni_env, uintptr_t ctx, char* uriCstr); +void* openStream(uintptr_t jni_env, uintptr_t ctx, char* uriCstr); +char* readStream(uintptr_t jni_env, uintptr_t ctx, void* stream, int len, int* total); +void* saveStream(uintptr_t jni_env, uintptr_t ctx, char* uriCstr, bool truncate); +void writeStream(uintptr_t jni_env, uintptr_t ctx, void* stream, char* data, int len); +void closeStream(uintptr_t jni_env, uintptr_t ctx, void* stream); +*/ +import "C" + +import ( + "errors" + "io" + "os" + "unsafe" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/driver/mobile/app" + "fyne.io/fyne/v2/storage" + "fyne.io/fyne/v2/storage/repository" +) + +type javaStream struct { + stream unsafe.Pointer // java.io.InputStream +} + +// Declare conformity to ReadCloser interface +var _ io.ReadCloser = (*javaStream)(nil) + +func (s *javaStream) Read(p []byte) (int, error) { + count := 0 + err := app.RunOnJVM(func(_, env, ctx uintptr) error { + cCount := C.int(0) + cBytes := unsafe.Pointer(C.readStream(C.uintptr_t(env), C.uintptr_t(ctx), s.stream, C.int(len(p)), &cCount)) + if cCount == -1 { + return io.EOF + } + defer C.free(cBytes) + count = int(cCount) // avoid sending -1 instead of 0 on completion + + bytes := C.GoBytes(cBytes, cCount) + for i := 0; i < int(count); i++ { + p[i] = bytes[i] + } + return nil + }) + + return int(count), err +} + +func (s *javaStream) Close() error { + app.RunOnJVM(func(_, env, ctx uintptr) error { + C.closeStream(C.uintptr_t(env), C.uintptr_t(ctx), s.stream) + + return nil + }) + + return nil +} + +func openStream(uri string) unsafe.Pointer { + uriStr := C.CString(uri) + defer C.free(unsafe.Pointer(uriStr)) + + var stream unsafe.Pointer + app.RunOnJVM(func(_, env, ctx uintptr) error { + streamPtr := C.openStream(C.uintptr_t(env), C.uintptr_t(ctx), uriStr) + if streamPtr == C.NULL { + return os.ErrNotExist + } + + stream = unsafe.Pointer(streamPtr) + return nil + }) + return stream +} + +func nativeFileOpen(f *fileOpen) (io.ReadCloser, error) { + if f.uri == nil || f.uri.String() == "" { + return nil, nil + } + + ret := openStream(f.uri.String()) + if ret == nil { + return nil, storage.ErrNotExists + } + + stream := &javaStream{} + stream.stream = ret + return stream, nil +} + +func saveStream(uri string, truncate bool) unsafe.Pointer { + uriStr := C.CString(uri) + defer C.free(unsafe.Pointer(uriStr)) + + var stream unsafe.Pointer + app.RunOnJVM(func(_, env, ctx uintptr) error { + streamPtr := C.saveStream(C.uintptr_t(env), C.uintptr_t(ctx), uriStr, C.bool(truncate)) + if streamPtr == C.NULL { + return os.ErrNotExist + } + + stream = unsafe.Pointer(streamPtr) + return nil + }) + return stream +} + +func nativeFileSave(f *fileSave, truncate bool) (io.WriteCloser, error) { + if f.uri == nil || f.uri.String() == "" { + return nil, nil + } + + ret := saveStream(f.uri.String(), truncate) + if ret == nil { + return nil, storage.ErrNotExists + } + + stream := &javaStream{} + stream.stream = ret + return stream, nil +} + +// Declare conformity to WriteCloser interface +var _ io.WriteCloser = (*javaStream)(nil) + +func (s *javaStream) Write(p []byte) (int, error) { + err := app.RunOnJVM(func(_, env, ctx uintptr) error { + C.writeStream(C.uintptr_t(env), C.uintptr_t(ctx), s.stream, (*C.char)(C.CBytes(p)), C.int(len(p))) + return nil + }) + + return len(p), err +} + +func deleteURI(u fyne.URI) error { + uriStr := C.CString(u.String()) + defer C.free(unsafe.Pointer(uriStr)) + + ok := false + app.RunOnJVM(func(_, env, ctx uintptr) error { + ok = bool(C.deleteURI(C.uintptr_t(env), C.uintptr_t(ctx), uriStr)) + return nil + }) + + if !ok { + return errors.New("failed to delete file " + u.String()) + } + return nil +} + +func existsURI(uri fyne.URI) (bool, error) { + uriStr := C.CString(uri.String()) + defer C.free(unsafe.Pointer(uriStr)) + + ok := false + app.RunOnJVM(func(_, env, ctx uintptr) error { + ok = bool(C.existsURI(C.uintptr_t(env), C.uintptr_t(ctx), uriStr)) + return nil + }) + + return ok, nil +} + +func registerRepository(d *driver) { + repo := &mobileFileRepo{} + repository.Register("file", repo) + repository.Register("content", repo) +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/file_desktop.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/file_desktop.go new file mode 100644 index 0000000..aac22de --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/file_desktop.go @@ -0,0 +1,35 @@ +//go:build !ios && !android + +package mobile + +import ( + "io" + + "fyne.io/fyne/v2" + intRepo "fyne.io/fyne/v2/internal/repository" + "fyne.io/fyne/v2/storage/repository" +) + +func deleteURI(_ fyne.URI) error { + // no-op as we use the internal FileRepository + return nil +} + +func existsURI(fyne.URI) (bool, error) { + // no-op as we use the internal FileRepository + return false, nil +} + +func nativeFileOpen(*fileOpen) (io.ReadCloser, error) { + // no-op as we use the internal FileRepository + return nil, nil +} + +func nativeFileSave(*fileSave, bool) (io.WriteCloser, error) { + // no-op as we use the internal FileRepository + return nil, nil +} + +func registerRepository(_ *driver) { + repository.Register("file", intRepo.NewFileRepository()) +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/file_ios.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/file_ios.go new file mode 100644 index 0000000..7e64c89 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/file_ios.go @@ -0,0 +1,158 @@ +//go:build ios + +package mobile + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Foundation + +#import +#import + +void iosDeletePath(const char* path); +bool iosExistsPath(const char* path); +void* iosParseUrl(const char* url); +const void* iosReadFromURL(void* url, int* len); + +const void* iosOpenFileWriter(void* url, bool truncate); +void iosCloseFileWriter(void* handle); +const int iosWriteToFile(void* handle, const void* bytes, int len); +*/ +import "C" + +import ( + "errors" + "io" + "unsafe" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/storage/repository" +) + +type secureReadCloser struct { + url unsafe.Pointer + closer func() + + data []byte + offset int +} + +// Declare conformity to ReadCloser interface +var _ io.ReadCloser = (*secureReadCloser)(nil) + +func (s *secureReadCloser) Read(p []byte) (int, error) { + if s.data == nil { + var length C.int + s.data = C.GoBytes(C.iosReadFromURL(s.url, &length), length) + } + + count := len(p) + remain := len(s.data) - s.offset + var err error + if count >= remain { + count = remain + err = io.EOF + } + + newOffset := s.offset + count + + o := 0 + for i := s.offset; i < newOffset; i++ { + p[o] = s.data[i] + o++ + } + s.offset = newOffset + return count, err +} + +func (s *secureReadCloser) Close() error { + if s.closer != nil { + s.closer() + } + s.url = nil + return nil +} + +type secureWriteCloser struct { + handle unsafe.Pointer + closer func() + + offset int +} + +// Declare conformity to WriteCloser interface +var _ io.WriteCloser = (*secureWriteCloser)(nil) + +func (s *secureWriteCloser) Write(p []byte) (int, error) { + count := int(C.iosWriteToFile(s.handle, C.CBytes(p), C.int(len(p)))) + s.offset += count + + return count, nil +} + +func (s *secureWriteCloser) Close() error { + if s.closer != nil { + s.closer() + } + C.iosCloseFileWriter(s.handle) + s.handle = nil + return nil +} + +func deleteURI(u fyne.URI) error { + if u.Scheme() != "file" { + return errors.New("cannot delete from " + u.Scheme() + " scheme on iOS") + } + + cStr := C.CString(u.Path()) + defer C.free(unsafe.Pointer(cStr)) + + C.iosDeletePath(cStr) + return nil +} + +func existsURI(u fyne.URI) (bool, error) { + if u.Scheme() != "file" { + return true, errors.New("cannot check existence of " + u.Scheme() + " scheme on iOS") + } + + cStr := C.CString(u.Path()) + defer C.free(unsafe.Pointer(cStr)) + + exists := C.iosExistsPath(cStr) + return bool(exists), nil +} + +func nativeFileOpen(f *fileOpen) (io.ReadCloser, error) { + if f.uri == nil || f.uri.String() == "" { + return nil, nil + } + + cStr := C.CString(f.uri.String()) + defer C.free(unsafe.Pointer(cStr)) + + url := C.iosParseUrl(cStr) + + fileStruct := &secureReadCloser{url: url, closer: f.done} + return fileStruct, nil +} + +func nativeFileSave(f *fileSave, truncate bool) (io.WriteCloser, error) { + if f.uri == nil || f.uri.String() == "" { + return nil, nil + } + + cStr := C.CString(f.uri.String()) + defer C.free(unsafe.Pointer(cStr)) + + url := C.iosParseUrl(cStr) + + handle := C.iosOpenFileWriter(url, C.bool(truncate)) + fileStruct := &secureWriteCloser{handle: handle, closer: f.done} + return fileStruct, nil +} + +func registerRepository(d *driver) { + repo := &mobileFileRepo{} + repository.Register("file", repo) +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/file_ios.m b/vendor/fyne.io/fyne/v2/internal/driver/mobile/file_ios.m new file mode 100644 index 0000000..01498ac --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/file_ios.m @@ -0,0 +1,62 @@ +//go:build ios + +#import + +#import + +void iosDeletePath(const char* path) { + NSString *pathStr = [NSString stringWithUTF8String:path]; + [[NSFileManager defaultManager] removeItemAtPath:pathStr error:nil]; +} + +bool iosExistsPath(const char* path) { + NSString *pathStr = [NSString stringWithUTF8String:path]; + return [[NSFileManager defaultManager] fileExistsAtPath:pathStr]; +} + +void* iosParseUrl(const char* url) { + NSString *urlStr = [NSString stringWithUTF8String:url]; + return [NSURL URLWithString:urlStr]; +} + +const void* iosReadFromURL(void* urlPtr, int* len) { + NSURL* url = (NSURL*)urlPtr; + NSData* data = [NSData dataWithContentsOfURL:url]; + + *len = data.length; + return data.bytes; +} + +const void* iosOpenFileWriter(void* urlPtr, bool truncate) { + NSURL* url = (NSURL*)urlPtr; + + if (truncate || ![[NSFileManager defaultManager] fileExistsAtPath:url.path]) { + [[NSFileManager defaultManager] createFileAtPath:url.path contents:nil attributes:nil]; + } + + NSError *err = nil; + // TODO handle error + NSFileHandle* handle = [NSFileHandle fileHandleForWritingToURL:url error:&err]; + + if (!truncate) { + [handle seekToEndOfFile]; + } + + return handle; +} + +void iosCloseFileWriter(void* handlePtr) { + NSFileHandle* handle = (NSFileHandle*)handlePtr; + + [handle synchronizeFile]; + [handle closeFile]; +} + + +const int iosWriteToFile(void* handlePtr, const void* bytes, int len) { + NSFileHandle* handle = (NSFileHandle*)handlePtr; + NSData *data = [NSData dataWithBytes:bytes length:len]; + + [handle writeData:data]; + return data.length; +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/folder.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/folder.go new file mode 100644 index 0000000..4e5dba0 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/folder.go @@ -0,0 +1,23 @@ +package mobile + +import ( + "errors" + + "fyne.io/fyne/v2" +) + +type lister struct { + fyne.URI +} + +func (l *lister) List() ([]fyne.URI, error) { + return listURI(l) +} + +func listerForURI(uri fyne.URI) (fyne.ListableURI, error) { + if !canListURI(uri) { + return nil, errors.New("specified URI is not listable") + } + + return &lister{uri}, nil +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/folder_android.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/folder_android.go new file mode 100644 index 0000000..c42844e --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/folder_android.go @@ -0,0 +1,75 @@ +//go:build android + +package mobile + +/* +#cgo LDFLAGS: -landroid -llog -lEGL -lGLESv2 + +#include +#include + +bool canListURI(uintptr_t jni_env, uintptr_t ctx, char* uriCstr); +bool createListableURI(uintptr_t jni_env, uintptr_t ctx, char* uriCstr); +char *listURI(uintptr_t jni_env, uintptr_t ctx, char* uriCstr); +*/ +import "C" + +import ( + "errors" + "strings" + "unsafe" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/driver/mobile/app" + "fyne.io/fyne/v2/storage" +) + +func canListURI(uri fyne.URI) bool { + uriStr := C.CString(uri.String()) + defer C.free(unsafe.Pointer(uriStr)) + listable := false + + app.RunOnJVM(func(_, env, ctx uintptr) error { + listable = bool(C.canListURI(C.uintptr_t(env), C.uintptr_t(ctx), uriStr)) + return nil + }) + + return listable +} + +func createListableURI(uri fyne.URI) error { + uriStr := C.CString(uri.String()) + defer C.free(unsafe.Pointer(uriStr)) + + ok := false + app.RunOnJVM(func(_, env, ctx uintptr) error { + ok = bool(C.createListableURI(C.uintptr_t(env), C.uintptr_t(ctx), uriStr)) + return nil + }) + + if ok { + return nil + } + return errors.New("failed to create directory") +} + +func listURI(uri fyne.URI) ([]fyne.URI, error) { + uriStr := C.CString(uri.String()) + defer C.free(unsafe.Pointer(uriStr)) + + var str *C.char + app.RunOnJVM(func(_, env, ctx uintptr) error { + str = C.listURI(C.uintptr_t(env), C.uintptr_t(ctx), uriStr) + return nil + }) + + parts := strings.Split(C.GoString(str), "|") + var list []fyne.URI + for _, part := range parts { + if len(part) == 0 { + continue + } + list = append(list, storage.NewURI(part)) + } + return list, nil +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/folder_desktop.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/folder_desktop.go new file mode 100644 index 0000000..33badb1 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/folder_desktop.go @@ -0,0 +1,22 @@ +//go:build !ios && !android + +package mobile + +import ( + "fyne.io/fyne/v2" +) + +func canListURI(fyne.URI) bool { + // no-op as we use the internal FileRepository + return false +} + +func createListableURI(fyne.URI) error { + // no-op as we use the internal FileRepository + return nil +} + +func listURI(fyne.URI) ([]fyne.URI, error) { + // no-op as we use the internal FileRepository + return nil, nil +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/folder_ios.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/folder_ios.go new file mode 100644 index 0000000..ba72828 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/folder_ios.go @@ -0,0 +1,59 @@ +//go:build ios + +package mobile + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Foundation + +#import +#import + +bool iosCanList(const char* url); +bool iosCreateListable(const char* url); +char* iosList(const char* url); +*/ +import "C" + +import ( + "errors" + "strings" + "unsafe" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/storage" +) + +func canListURI(uri fyne.URI) bool { + uriStr := C.CString(uri.String()) + defer C.free(unsafe.Pointer(uriStr)) + + return bool(C.iosCanList(uriStr)) +} + +func createListableURI(uri fyne.URI) error { + uriStr := C.CString(uri.String()) + defer C.free(unsafe.Pointer(uriStr)) + + ok := bool(C.iosCreateListable(uriStr)) + if ok { + return nil + } + return errors.New("failed to create directory") +} + +func listURI(uri fyne.URI) ([]fyne.URI, error) { + uriStr := C.CString(uri.String()) + defer C.free(unsafe.Pointer(uriStr)) + + str := C.iosList(uriStr) + parts := strings.Split(C.GoString(str), "|") + var list []fyne.URI + for _, part := range parts { + if len(part) == 0 { + continue + } + list = append(list, storage.NewURI(part)) + } + return list, nil +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/folder_ios.m b/vendor/fyne.io/fyne/v2/internal/driver/mobile/folder_ios.m new file mode 100644 index 0000000..d8b890c --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/folder_ios.m @@ -0,0 +1,29 @@ +//go:build ios + +#import + +#import + +NSArray *listForURL(const char* urlCstr) { + NSString *urlStr = [NSString stringWithUTF8String:urlCstr]; + NSURL *url = [NSURL URLWithString:urlStr]; + + return [[NSFileManager defaultManager] contentsOfDirectoryAtURL:url includingPropertiesForKeys:nil options:0 error:nil]; +} + +bool iosCanList(const char* url) { + return listForURL(url) != nil; +} + +bool iosCreateListable(const char* urlCstr) { + NSString *urlStr = [NSString stringWithUTF8String:urlCstr]; + NSURL *url = [NSURL URLWithString:urlStr]; + + return [[NSFileManager defaultManager] createDirectoryAtURL:url withIntermediateDirectories:YES attributes:nil error:nil]; +} + +char* iosList(const char* url) { + NSArray *children = listForURL(url); + + return (char *) [[children componentsJoinedByString:@"|"] UTF8String]; +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/consts.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/consts.go new file mode 100644 index 0000000..b6be0e0 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/consts.go @@ -0,0 +1,80 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gl + +/* +Partially generated from the Khronos OpenGL API specification in XML +format, which is covered by the license: + + Copyright (c) 2013-2014 The Khronos Group Inc. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and/or associated documentation files (the + "Materials"), to deal in the Materials without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Materials, and to + permit persons to whom the Materials are furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Materials. + + THE MATERIALS ARE 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 + MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. + +*/ + +// Contains Khronos OpenGL API specification constants. +const ( + False = 0 + True = 1 + One = 1 + Triangles = 0x0004 + TriangleStrip = 0x0005 + SrcAlpha = 0x0302 + OneMinusSrcAlpha = 0x0303 + Front = 0x0404 + DepthTest = 0x0B71 + Blend = 0x0BE2 + ScissorTest = 0x0C11 + Texture2D = 0x0DE1 + + UnsignedByte = 0x1401 + Float = 0x1406 + RED = 0x1903 + RGBA = 0x1908 + + Nearest = 0x2600 + Linear = 0x2601 + TextureMagFilter = 0x2800 + TextureMinFilter = 0x2801 + TextureWrapS = 0x2802 + TextureWrapT = 0x2803 + + ConstantAlpha = 0x8003 + OneMinusConstantAlpha = 0x8004 + ClampToEdge = 0x812F + Texture0 = 0x84C0 + StaticDraw = 0x88E4 + DynamicDraw = 0x88E8 + FragmentShader = 0x8B30 + VertexShader = 0x8B31 + AttachedShaders = 0x8B85 + ActiveUniformMaxLength = 0x8B87 + ActiveAttributeMaxLength = 0x8B8A + ArrayBuffer = 0x8892 + CompileStatus = 0x8B81 + LinkStatus = 0x8B82 + InfoLogLength = 0x8B84 + ShaderSourceLength = 0x8B88 + + DepthBufferBit = 0x00000100 + ColorBufferBit = 0x00004000 +) diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/dll_windows.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/dll_windows.go new file mode 100644 index 0000000..a7f32a6 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/dll_windows.go @@ -0,0 +1,242 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gl + +import ( + "archive/tar" + "compress/gzip" + "debug/pe" + "fmt" + "io" + "log" + "net/http" + "os" + "path/filepath" + "runtime" +) + +var debug = log.New(io.Discard, "gl: ", log.LstdFlags) + +func downloadDLLs() (path string, err error) { + url := "https://dl.google.com/go/mobile/angle-bd3f8780b-" + runtime.GOARCH + ".tgz" + debug.Printf("downloading %s", url) + resp, err := http.Get(url) + if err != nil { + return "", fmt.Errorf("gl: %v", err) + } + defer func() { + err2 := resp.Body.Close() + if err == nil && err2 != nil { + err = fmt.Errorf("gl: error reading body from %v: %v", url, err2) + } + }() + if resp.StatusCode != http.StatusOK { + err := fmt.Errorf("gl: error fetching %v, status: %v", url, resp.Status) + return "", err + } + + r, err := gzip.NewReader(resp.Body) + if err != nil { + return "", fmt.Errorf("gl: error reading gzip from %v: %v", url, err) + } + tr := tar.NewReader(r) + var bytesGLESv2, bytesEGL, bytesD3DCompiler []byte + for { + header, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return "", fmt.Errorf("gl: error reading tar from %v: %v", url, err) + } + switch header.Name { + case "angle-" + runtime.GOARCH + "/libglesv2.dll": + bytesGLESv2, err = io.ReadAll(tr) + case "angle-" + runtime.GOARCH + "/libegl.dll": + bytesEGL, err = io.ReadAll(tr) + case "angle-" + runtime.GOARCH + "/d3dcompiler_47.dll": + bytesD3DCompiler, err = io.ReadAll(tr) + default: // skip + } + if err != nil { + return "", fmt.Errorf("gl: error reading %v from %v: %v", header.Name, url, err) + } + } + if len(bytesGLESv2) == 0 || len(bytesEGL) == 0 || len(bytesD3DCompiler) == 0 { + return "", fmt.Errorf("gl: did not find all DLLs in %v", url) + } + + writeDLLs := func(path string) error { + if err := os.WriteFile(filepath.Join(path, "libglesv2.dll"), bytesGLESv2, 0o755); err != nil { + return fmt.Errorf("gl: cannot install ANGLE: %v", err) + } + if err := os.WriteFile(filepath.Join(path, "libegl.dll"), bytesEGL, 0o755); err != nil { + return fmt.Errorf("gl: cannot install ANGLE: %v", err) + } + if err := os.WriteFile(filepath.Join(path, "d3dcompiler_47.dll"), bytesD3DCompiler, 0o755); err != nil { + return fmt.Errorf("gl: cannot install ANGLE: %v", err) + } + return nil + } + + // First, we attempt to install these DLLs in LOCALAPPDATA/Shiny. + // + // Traditionally we would use the system32 directory, but it is + // no longer writable by normal programs. + os.MkdirAll(appdataPath(), 0o775) + if err := writeDLLs(appdataPath()); err == nil { + return appdataPath(), nil + } + debug.Printf("DLLs could not be written to %s", appdataPath()) + + // Second, install in GOPATH/pkg if it exists. + gopath := os.Getenv("GOPATH") + gopathpkg := filepath.Join(gopath, "pkg") + if _, err := os.Stat(gopathpkg); err == nil && gopath != "" { + if err := writeDLLs(gopathpkg); err == nil { + return gopathpkg, nil + } + } + debug.Printf("DLLs could not be written to GOPATH") + + // Third, pick a temporary directory. + tmp := os.TempDir() + if err := writeDLLs(tmp); err != nil { + return "", fmt.Errorf("gl: unable to install ANGLE DLLs: %v", err) + } + return tmp, nil +} + +func appdataPath() string { + return filepath.Join(os.Getenv("LOCALAPPDATA"), "GoGL", runtime.GOARCH) +} + +func containsDLLs(dir string) bool { + compatible := func(name string) bool { + file, err := pe.Open(filepath.Join(dir, name)) + if err != nil { + return false + } + defer file.Close() + + switch file.Machine { + case pe.IMAGE_FILE_MACHINE_AMD64: + return "amd64" == runtime.GOARCH + case pe.IMAGE_FILE_MACHINE_ARM: + return "arm" == runtime.GOARCH + case pe.IMAGE_FILE_MACHINE_I386: + return "386" == runtime.GOARCH + } + return false + } + + return compatible("libglesv2.dll") && compatible("libegl.dll") && compatible("d3dcompiler_47.dll") +} + +func chromePath() string { + // dlls are stored in: + // //libglesv2.dll + + installdirs := []string{ + // Chrome User + filepath.Join(os.Getenv("LOCALAPPDATA"), "Google", "Chrome", "Application"), + // Chrome System + filepath.Join(os.Getenv("ProgramFiles(x86)"), "Google", "Chrome", "Application"), + // Chromium + filepath.Join(os.Getenv("LOCALAPPDATA"), "Chromium", "Application"), + // Chrome Canary + filepath.Join(os.Getenv("LOCALAPPDATA"), "Google", "Chrome SxS", "Application"), + } + + for _, installdir := range installdirs { + versiondirs, err := os.ReadDir(installdir) + if err != nil { + continue + } + + for _, versiondir := range versiondirs { + if !versiondir.IsDir() { + continue + } + + versionpath := filepath.Join(installdir, versiondir.Name()) + if containsDLLs(versionpath) { + return versionpath + } + } + } + + return "" +} + +func findDLLs() (err error) { + load := func(path string) (bool, error) { + if path != "" { + // don't try to start when one of the files is missing + if !containsDLLs(path) { + return false, nil + } + + LibD3DCompiler.Name = filepath.Join(path, filepath.Base(LibD3DCompiler.Name)) + LibGLESv2.Name = filepath.Join(path, filepath.Base(LibGLESv2.Name)) + LibEGL.Name = filepath.Join(path, filepath.Base(LibEGL.Name)) + } + + if err := LibGLESv2.Load(); err == nil { + if err := LibEGL.Load(); err != nil { + return false, fmt.Errorf("gl: loaded libglesv2 but not libegl: %v", err) + } + if err := LibD3DCompiler.Load(); err != nil { + return false, fmt.Errorf("gl: loaded libglesv2, libegl but not d3dcompiler: %v", err) + } + if path == "" { + debug.Printf("DLLs found") + } else { + debug.Printf("DLLs found in: %q", path) + } + return true, nil + } + + return false, nil + } + + // Look in the system directory. + if ok, err := load(""); ok || err != nil { + return err + } + + // Look in the AppData directory. + if ok, err := load(appdataPath()); ok || err != nil { + return err + } + + // Look for a Chrome installation + if dir := chromePath(); dir != "" { + if ok, err := load(dir); ok || err != nil { + return err + } + } + + // Look in GOPATH/pkg. + if ok, err := load(filepath.Join(os.Getenv("GOPATH"), "pkg")); ok || err != nil { + return err + } + + // Look in temporary directory. + if ok, err := load(os.TempDir()); ok || err != nil { + return err + } + + // Download the DLL binary. + path, err := downloadDLLs() + if err != nil { + return err + } + debug.Printf("DLLs written to %s", path) + if ok, err := load(path); !ok || err != nil { + return fmt.Errorf("gl: unable to load ANGLE after installation: %v", err) + } + return nil +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/doc.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/doc.go new file mode 100644 index 0000000..584800c --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/doc.go @@ -0,0 +1,44 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package gl implements Go bindings for OpenGL ES 2.0 and ES 3.0. + +The GL functions are defined on a Context object that is responsible for +tracking a GL context. Typically a windowing system package (such as +golang.org/x/exp/shiny/screen) will call NewContext and provide +a gl.Context for a user application. + +If the gl package is compiled on a platform capable of supporting ES 3.0, +the gl.Context object also implements gl.Context3. + +The bindings are deliberately minimal, staying as close the C API as +possible. The semantics of each function maps onto functions +described in the Khronos documentation: + +https://www.khronos.org/opengles/sdk/docs/man/ + +One notable departure from the C API is the introduction of types +to represent common uses of GLint: Texture, Surface, Buffer, etc. +*/ +package gl // import "fyne.io/fyne/v2/internal/driver/mobile/gl" + +/* +Implementation details. + +All GL function calls fill out a C.struct_fnargs and drop it on the work +queue. The Start function drains the work queue and hands over a batch +of calls to C.process which runs them. This allows multiple GL calls to +be executed in a single cgo call. + +A GL call is marked as blocking if it returns a value, or if it takes a +Go pointer. In this case the call will not return until C.process sends a +value on the retvalue channel. + +This implementation ensures any goroutine can make GL calls, but it does +not make the GL interface safe for simultaneous use by multiple goroutines. +For the purpose of analyzing this code for race conditions, picture two +separate goroutines: one blocked on gl.Start, and another making calls to +the gl package exported functions. +*/ diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/fn.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/fn.go new file mode 100644 index 0000000..5769cbe --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/fn.go @@ -0,0 +1,96 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gl + +import "unsafe" + +type call struct { + args fnargs + parg unsafe.Pointer + blocking bool +} + +type fnargs struct { + fn glfn + + a0 uintptr + a1 uintptr + a2 uintptr + a3 uintptr + a4 uintptr + a5 uintptr + a6 uintptr + a7 uintptr + a8 uintptr + a9 uintptr +} + +type glfn int + +const ( + glfnUNDEFINED glfn = iota + glfnActiveTexture + glfnAttachShader + glfnBindBuffer + glfnBindTexture + glfnBindVertexArray + glfnBlendColor + glfnBlendFunc + glfnBufferData + glfnBufferSubData + glfnClear + glfnClearColor + glfnCompileShader + glfnCreateProgram + glfnCreateShader + glfnDeleteBuffer + glfnDeleteTexture + glfnDisable + glfnDrawArrays + glfnEnable + glfnEnableVertexAttribArray + glfnFlush + glfnGenBuffer + glfnGenTexture + glfnGenVertexArray + glfnGetAttribLocation + glfnGetError + glfnGetProgramInfoLog + glfnGetProgramiv + glfnGetShaderInfoLog + glfnGetShaderSource + glfnGetShaderiv + glfnGetTexParameteriv + glfnGetUniformLocation + glfnLinkProgram + glfnReadPixels + glfnScissor + glfnShaderSource + glfnTexImage2D + glfnTexParameteri + glfnUniform1f + glfnUniform2f + glfnUniform4f + glfnUniform4fv + glfnUseProgram + glfnVertexAttribPointer + glfnViewport +) + +func goString(buf []byte) string { + for i, b := range buf { + if b == 0 { + return string(buf[:i]) + } + } + panic("buf is not NUL-terminated") +} + +func glBoolean(b bool) uintptr { + if b { + return True + } + return False +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/gl.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/gl.go new file mode 100644 index 0000000..b31ae47 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/gl.go @@ -0,0 +1,561 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build darwin || linux || openbsd || freebsd || windows + +package gl + +// TODO(crawshaw): should functions on specific types become methods? E.g. +// func (t Texture) Bind(target Enum) +// this seems natural in Go, but moves us slightly +// further away from the underlying OpenGL spec. + +import ( + "math" + "unsafe" +) + +func (ctx *context) ActiveTexture(texture Enum) { + ctx.enqueue(call{ + args: fnargs{ + fn: glfnActiveTexture, + a0: texture.c(), + }, + }) +} + +func (ctx *context) AttachShader(p Program, s Shader) { + ctx.enqueue(call{ + args: fnargs{ + fn: glfnAttachShader, + a0: p.c(), + a1: s.c(), + }, + }) +} + +func (ctx *context) BindBuffer(target Enum, b Buffer) { + ctx.enqueue(call{ + args: fnargs{ + fn: glfnBindBuffer, + a0: target.c(), + a1: b.c(), + }, + }) +} + +func (ctx *context) BindTexture(target Enum, t Texture) { + ctx.enqueue(call{ + args: fnargs{ + fn: glfnBindTexture, + a0: target.c(), + a1: t.c(), + }, + }) +} + +func (ctx *context) BindVertexArray(va VertexArray) { + ctx.enqueue(call{ + args: fnargs{ + fn: glfnBindVertexArray, + a0: va.c(), + }, + }) +} + +func (ctx *context) BlendColor(red, green, blue, alpha float32) { + ctx.enqueue(call{ + args: fnargs{ + fn: glfnBlendColor, + a0: uintptr(math.Float32bits(red)), + a1: uintptr(math.Float32bits(green)), + a2: uintptr(math.Float32bits(blue)), + a3: uintptr(math.Float32bits(alpha)), + }, + }) +} + +func (ctx *context) BlendFunc(sfactor, dfactor Enum) { + ctx.enqueue(call{ + args: fnargs{ + fn: glfnBlendFunc, + a0: sfactor.c(), + a1: dfactor.c(), + }, + }) +} + +func (ctx *context) BufferData(target Enum, src []byte, usage Enum) { + parg := unsafe.Pointer(nil) + if len(src) > 0 { + parg = unsafe.Pointer(&src[0]) + } + ctx.enqueue(call{ + args: fnargs{ + fn: glfnBufferData, + a0: target.c(), + a1: uintptr(len(src)), + a2: usage.c(), + }, + parg: parg, + }) +} + +func (ctx *context) BufferSubData(target Enum, src []byte) { + parg := unsafe.Pointer(nil) + if len(src) > 0 { + parg = unsafe.Pointer(&src[0]) + } + ctx.enqueue(call{ + args: fnargs{ + fn: glfnBufferSubData, + a0: target.c(), + a1: 0, + a2: uintptr(len(src)), + }, + parg: parg, + }) +} + +func (ctx *context) Clear(mask Enum) { + ctx.enqueue(call{ + args: fnargs{ + fn: glfnClear, + a0: uintptr(mask), + }, + }) +} + +func (ctx *context) ClearColor(red, green, blue, alpha float32) { + ctx.enqueue(call{ + args: fnargs{ + fn: glfnClearColor, + a0: uintptr(math.Float32bits(red)), + a1: uintptr(math.Float32bits(green)), + a2: uintptr(math.Float32bits(blue)), + a3: uintptr(math.Float32bits(alpha)), + }, + }) +} + +func (ctx *context) CompileShader(s Shader) { + ctx.enqueue(call{ + args: fnargs{ + fn: glfnCompileShader, + a0: s.c(), + }, + }) +} + +func (ctx *context) CreateBuffer() Buffer { + return Buffer{Value: uint32(ctx.enqueue(call{ + args: fnargs{ + fn: glfnGenBuffer, + }, + blocking: true, + }))} +} + +func (ctx *context) CreateProgram() Program { + return Program{ + Init: true, + Value: uint32(ctx.enqueue(call{ + args: fnargs{ + fn: glfnCreateProgram, + }, + blocking: true, + }, + )), + } +} + +func (ctx *context) CreateShader(ty Enum) Shader { + return Shader{Value: uint32(ctx.enqueue(call{ + args: fnargs{ + fn: glfnCreateShader, + a0: uintptr(ty), + }, + blocking: true, + }))} +} + +func (ctx *context) CreateTexture() Texture { + return Texture{Value: uint32(ctx.enqueue(call{ + args: fnargs{ + fn: glfnGenTexture, + }, + blocking: true, + }))} +} + +func (ctx *context) CreateVertexArray() VertexArray { + return VertexArray{Value: uint32(ctx.enqueue(call{ + args: fnargs{ + fn: glfnGenVertexArray, + }, + blocking: true, + }))} +} + +func (ctx *context) DeleteBuffer(v Buffer) { + ctx.enqueue(call{ + args: fnargs{ + fn: glfnDeleteBuffer, + a0: v.c(), + }, + }) +} + +func (ctx *context) DeleteTexture(v Texture) { + ctx.enqueue(call{ + args: fnargs{ + fn: glfnDeleteTexture, + a0: v.c(), + }, + }) +} + +func (ctx *context) Disable(cap Enum) { + ctx.enqueue(call{ + args: fnargs{ + fn: glfnDisable, + a0: cap.c(), + }, + }) +} + +func (ctx *context) DrawArrays(mode Enum, first, count int) { + ctx.enqueue(call{ + args: fnargs{ + fn: glfnDrawArrays, + a0: mode.c(), + a1: uintptr(first), + a2: uintptr(count), + }, + }) +} + +func (ctx *context) Enable(cap Enum) { + ctx.enqueue(call{ + args: fnargs{ + fn: glfnEnable, + a0: cap.c(), + }, + }) +} + +func (ctx *context) EnableVertexAttribArray(a Attrib) { + ctx.enqueue(call{ + args: fnargs{ + fn: glfnEnableVertexAttribArray, + a0: a.c(), + }, + }) +} + +func (ctx *context) Flush() { + ctx.enqueue(call{ + args: fnargs{ + fn: glfnFlush, + }, + blocking: true, + }) +} + +func (ctx *context) GetAttribLocation(p Program, name string) Attrib { + s, free := ctx.cString(name) + defer free() + return Attrib{Value: uint(ctx.enqueue(call{ + args: fnargs{ + fn: glfnGetAttribLocation, + a0: p.c(), + a1: s, + }, + blocking: true, + }))} +} + +func (ctx *context) GetError() Enum { + return Enum(ctx.enqueue(call{ + args: fnargs{ + fn: glfnGetError, + }, + blocking: true, + })) +} + +func (ctx *context) GetProgrami(p Program, pname Enum) int { + return int(ctx.enqueue(call{ + args: fnargs{ + fn: glfnGetProgramiv, + a0: p.c(), + a1: pname.c(), + }, + blocking: true, + })) +} + +func (ctx *context) GetProgramInfoLog(p Program) string { + infoLen := ctx.GetProgrami(p, InfoLogLength) + if infoLen == 0 { + return "" + } + buf := make([]byte, infoLen) + + ctx.enqueue(call{ + args: fnargs{ + fn: glfnGetProgramInfoLog, + a0: p.c(), + a1: uintptr(infoLen), + }, + parg: unsafe.Pointer(&buf[0]), + blocking: true, + }) + + return goString(buf) +} + +func (ctx *context) GetShaderi(s Shader, pname Enum) int { + return int(ctx.enqueue(call{ + args: fnargs{ + fn: glfnGetShaderiv, + a0: s.c(), + a1: pname.c(), + }, + blocking: true, + })) +} + +func (ctx *context) GetShaderInfoLog(s Shader) string { + infoLen := ctx.GetShaderi(s, InfoLogLength) + if infoLen == 0 { + return "" + } + buf := make([]byte, infoLen) + + ctx.enqueue(call{ + args: fnargs{ + fn: glfnGetShaderInfoLog, + a0: s.c(), + a1: uintptr(infoLen), + }, + parg: unsafe.Pointer(&buf[0]), + blocking: true, + }) + + return goString(buf) +} + +func (ctx *context) GetShaderSource(s Shader) string { + sourceLen := ctx.GetShaderi(s, ShaderSourceLength) + if sourceLen == 0 { + return "" + } + buf := make([]byte, sourceLen) + + ctx.enqueue(call{ + args: fnargs{ + fn: glfnGetShaderSource, + a0: s.c(), + a1: uintptr(sourceLen), + }, + parg: unsafe.Pointer(&buf[0]), + blocking: true, + }) + + return goString(buf) +} + +func (ctx *context) GetTexParameteriv(dst []int32, target, pname Enum) { + ctx.enqueue(call{ + args: fnargs{ + fn: glfnGetTexParameteriv, + a0: target.c(), + a1: pname.c(), + }, + blocking: true, + }) +} + +func (ctx *context) GetUniformLocation(p Program, name string) Uniform { + s, free := ctx.cString(name) + defer free() + return Uniform{Value: int32(ctx.enqueue(call{ + args: fnargs{ + fn: glfnGetUniformLocation, + a0: p.c(), + a1: s, + }, + blocking: true, + }))} +} + +func (ctx *context) LinkProgram(p Program) { + ctx.enqueue(call{ + args: fnargs{ + fn: glfnLinkProgram, + a0: p.c(), + }, + }) +} + +func (ctx *context) ReadPixels(dst []byte, x, y, width, height int, format, ty Enum) { + ctx.enqueue(call{ + args: fnargs{ + fn: glfnReadPixels, + // TODO(crawshaw): support PIXEL_PACK_BUFFER in GLES3, uses offset. + a0: uintptr(x), + a1: uintptr(y), + a2: uintptr(width), + a3: uintptr(height), + a4: format.c(), + a5: ty.c(), + }, + parg: unsafe.Pointer(&dst[0]), + blocking: true, + }) +} + +func (ctx *context) Scissor(x, y, width, height int32) { + ctx.enqueue(call{ + args: fnargs{ + fn: glfnScissor, + a0: uintptr(x), + a1: uintptr(y), + a2: uintptr(width), + a3: uintptr(height), + }, + }) +} + +func (ctx *context) ShaderSource(s Shader, src string) { + strp, free := ctx.cStringPtr(src) + defer free() + ctx.enqueue(call{ + args: fnargs{ + fn: glfnShaderSource, + a0: s.c(), + a1: 1, + a2: strp, + }, + blocking: true, + }) +} + +func (ctx *context) TexImage2D(target Enum, level int, internalFormat int, width, height int, format Enum, ty Enum, data []byte) { + // It is common to pass TexImage2D a nil data, indicating that a + // bound GL buffer is being used as the source. In that case, it + // is not necessary to block. + parg := unsafe.Pointer(nil) + if len(data) > 0 { + parg = unsafe.Pointer(&data[0]) + } + + ctx.enqueue(call{ + args: fnargs{ + fn: glfnTexImage2D, + // TODO(crawshaw): GLES3 offset for PIXEL_UNPACK_BUFFER and PIXEL_PACK_BUFFER. + a0: target.c(), + a1: uintptr(level), + a2: uintptr(internalFormat), + a3: uintptr(width), + a4: uintptr(height), + a5: format.c(), + a6: ty.c(), + }, + parg: parg, + blocking: parg != nil, + }) +} + +func (ctx *context) TexParameteri(target, pname Enum, param int) { + ctx.enqueue(call{ + args: fnargs{ + fn: glfnTexParameteri, + a0: target.c(), + a1: pname.c(), + a2: uintptr(param), + }, + }) +} + +func (ctx *context) Uniform1f(dst Uniform, v float32) { + ctx.enqueue(call{ + args: fnargs{ + fn: glfnUniform1f, + a0: dst.c(), + a1: uintptr(math.Float32bits(v)), + }, + }) +} + +func (ctx *context) Uniform2f(dst Uniform, v0, v1 float32) { + ctx.enqueue(call{ + args: fnargs{ + fn: glfnUniform2f, + a0: dst.c(), + a1: uintptr(math.Float32bits(v0)), + a2: uintptr(math.Float32bits(v1)), + }, + }) +} + +func (ctx *context) Uniform4f(dst Uniform, v0, v1, v2, v3 float32) { + ctx.enqueue(call{ + args: fnargs{ + fn: glfnUniform4f, + a0: dst.c(), + a1: uintptr(math.Float32bits(v0)), + a2: uintptr(math.Float32bits(v1)), + a3: uintptr(math.Float32bits(v2)), + a4: uintptr(math.Float32bits(v3)), + }, + }) +} + +func (ctx *context) Uniform4fv(dst Uniform, src []float32) { + ctx.enqueue(call{ + args: fnargs{ + fn: glfnUniform4fv, + a0: dst.c(), + a1: uintptr(len(src) / 4), + }, + parg: unsafe.Pointer(&src[0]), + }) +} + +func (ctx *context) UseProgram(p Program) { + ctx.enqueue(call{ + args: fnargs{ + fn: glfnUseProgram, + a0: p.c(), + }, + }) +} + +func (ctx *context) VertexAttribPointer(dst Attrib, size int, ty Enum, normalized bool, stride, offset int) { + ctx.enqueue(call{ + args: fnargs{ + fn: glfnVertexAttribPointer, + a0: dst.c(), + a1: uintptr(size), + a2: ty.c(), + a3: glBoolean(normalized), + a4: uintptr(stride), + a5: uintptr(offset), + }, + }) +} + +func (ctx *context) Viewport(x, y, width, height int) { + ctx.enqueue(call{ + args: fnargs{ + fn: glfnViewport, + a0: uintptr(x), + a1: uintptr(y), + a2: uintptr(width), + a3: uintptr(height), + }, + }) +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/interface.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/interface.go new file mode 100644 index 0000000..67e6841 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/interface.go @@ -0,0 +1,285 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gl + +// Context is an OpenGL ES context. +// +// A Context has a method for every GL function supported by ES 2 or later. +// In a program compiled with ES 3 support. +// +// Calls are not safe for concurrent use. However calls can be made from +// any goroutine, the gl package removes the notion of thread-local +// context. +// +// Contexts are independent. Two contexts can be used concurrently. +type Context interface { + // ActiveTexture sets the active texture unit. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glActiveTexture.xhtml + ActiveTexture(texture Enum) + + // AttachShader attaches a shader to a program. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glAttachShader.xhtml + AttachShader(p Program, s Shader) + + // BindBuffer binds a buffer. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glBindBuffer.xhtml + BindBuffer(target Enum, b Buffer) + // BindTexture binds a texture. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glBindTexture.xhtml + BindTexture(target Enum, t Texture) + + // BindVertexArray binds a vertex array. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glBindVertexArray.xhtml + BindVertexArray(rb VertexArray) + + // BlendColor sets the blend color. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glBlendColor.xhtml + BlendColor(red, green, blue, alpha float32) + + // BlendFunc sets the pixel blending factors. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glBlendFunc.xhtml + BlendFunc(sfactor, dfactor Enum) + + // BufferData creates a new data store for the bound buffer object. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glBufferData.xhtml + BufferData(target Enum, src []byte, usage Enum) + + // BufferSubData updates the data store for a bound buffer object. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glBufferSubData.xhtml + BufferSubData(target Enum, src []byte) + + // Clear clears the window. + // + // The behavior of Clear is influenced by the pixel ownership test, + // the scissor test, dithering, and the buffer writemasks. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glClear.xhtml + Clear(mask Enum) + + // ClearColor specifies the RGBA values used to clear color buffers. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glClearColor.xhtml + ClearColor(red, green, blue, alpha float32) + + // CompileShader compiles the source code of s. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glCompileShader.xhtml + CompileShader(s Shader) + + // CreateBuffer creates a buffer object. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glGenBuffers.xhtml + CreateBuffer() Buffer + + // CreateProgram creates a new empty program object. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glCreateProgram.xhtml + CreateProgram() Program + + // CreateShader creates a new empty shader object. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glCreateShader.xhtml + CreateShader(ty Enum) Shader + + // CreateTexture creates a texture object. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glGenTextures.xhtml + CreateTexture() Texture + + // CreateTVertexArray creates a vertex array. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glGenVertexArrays.xhtml + CreateVertexArray() VertexArray + // DeleteBuffer deletes the given buffer object. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glDeleteBuffers.xhtml + DeleteBuffer(v Buffer) + + // DeleteTexture deletes the given texture object. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glDeleteTextures.xhtml + DeleteTexture(v Texture) + + // Disable disables various GL capabilities. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glDisable.xhtml + Disable(cap Enum) + + // DrawArrays renders geometric primitives from the bound data. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glDrawArrays.xhtml + DrawArrays(mode Enum, first, count int) + + // Enable enables various GL capabilities. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glEnable.xhtml + Enable(cap Enum) + + // EnableVertexAttribArray enables a vertex attribute array. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glEnableVertexAttribArray.xhtml + EnableVertexAttribArray(a Attrib) + // Flush empties all buffers. It does not block. + // + // An OpenGL implementation may buffer network communication, + // the command stream, or data inside the graphics accelerator. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glFlush.xhtml + Flush() + + // GetAttribLocation returns the location of an attribute variable. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glGetAttribLocation.xhtml + GetAttribLocation(p Program, name string) Attrib + + // GetError returns the next error. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glGetError.xhtml + GetError() Enum + + // GetProgrami returns a parameter value for a shader. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glGetProgramiv.xhtml + GetProgrami(p Program, pname Enum) int + + // GetProgramInfoLog returns the information log for a shader. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glGetProgramInfoLog.xhtml + GetProgramInfoLog(p Program) string + + // GetShaderi returns a parameter value for a shader. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glGetShaderiv.xhtml + GetShaderi(s Shader, pname Enum) int + + // GetShaderInfoLog returns the information log for a shader. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glGetShaderInfoLog.xhtml + GetShaderInfoLog(s Shader) string + + // GetUniformLocation returns the location of a uniform variable. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glGetUniformLocation.xhtml + GetUniformLocation(p Program, name string) Uniform + + // LinkProgram links the specified program. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glLinkProgram.xhtml + LinkProgram(p Program) + + // ReadPixels returns pixel data from a buffer. + // + // In GLES 3, the source buffer is controlled with ReadBuffer. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glReadPixels.xhtml + ReadPixels(dst []byte, x, y, width, height int, format, ty Enum) + + // Scissor defines the scissor box rectangle, in window coordinates. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glScissor.xhtml + Scissor(x, y, width, height int32) + + // ShaderSource sets the source code of s to the given source code. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glShaderSource.xhtml + ShaderSource(s Shader, src string) + // TexImage2D writes a 2D texture image. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glTexImage2D.xhtml + TexImage2D(target Enum, level int, internalFormat int, width, height int, format Enum, ty Enum, data []byte) + + // TexParameteri sets an integer texture parameter. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glTexParameter.xhtml + TexParameteri(target, pname Enum, param int) + + // Uniform1f writes a float uniform variable. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glUniform.xhtml + Uniform1f(dst Uniform, v float32) + + // Uniform2f writes a vec2 uniform variable. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glUniform.xhtml + Uniform2f(dst Uniform, v0, v1 float32) + + // Uniform4f writes a vec4 uniform variable. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glUniform.xhtml + Uniform4f(dst Uniform, v0, v1, v2, v3 float32) + + // Uniform4fv writes a vec4 uniform array of len(src)/4 elements. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glUniform.xhtml + Uniform4fv(dst Uniform, src []float32) + + // UseProgram sets the active program. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glUseProgram.xhtml + UseProgram(p Program) + + // VertexAttribPointer uses a bound buffer to define vertex attribute data. + // + // Direct use of VertexAttribPointer to load data into OpenGL is not + // supported via the Go bindings. Instead, use BindBuffer with an + // ARRAY_BUFFER and then fill it using BufferData. + // + // The size argument specifies the number of components per attribute, + // between 1-4. The stride argument specifies the byte offset between + // consecutive vertex attributes. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glVertexAttribPointer.xhtml + VertexAttribPointer(dst Attrib, size int, ty Enum, normalized bool, stride, offset int) + + // Viewport sets the viewport, an affine transformation that + // normalizes device coordinates to window coordinates. + // + // http://www.khronos.org/opengles/sdk/docs/man3/html/glViewport.xhtml + Viewport(x, y, width, height int) +} + +// Worker is used by display driver code to execute OpenGL calls. +// +// Typically display driver code creates a gl.Context for an application, +// and along with it establishes a locked OS thread to execute the cgo +// calls: +// +// go func() { +// runtime.LockOSThread() +// // ... platform-specific cgo call to bind a C OpenGL context +// // into thread-local storage. +// +// glctx, worker := gl.NewContext() +// workAvailable := worker.WorkAvailable() +// go userAppCode(glctx) +// for { +// select { +// case <-workAvailable: +// worker.DoWork() +// case <-drawEvent: +// // ... platform-specific cgo call to draw screen +// } +// } +// }() +// +// This interface is an internal implementation detail and should only be used +// by the package responsible for managing the screen. +type Worker interface { + // WorkAvailable returns a channel that communicates when DoWork should be + // called. + WorkAvailable() <-chan struct{} + + // DoWork performs any pending OpenGL calls. + DoWork() +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/types.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/types.go new file mode 100644 index 0000000..7ac3872 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/types.go @@ -0,0 +1,90 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build darwin || linux || openbsd || freebsd || windows + +package gl + +import "fmt" + +// Enum is equivalent to GLenum, and is normally used with one of the +// constants defined in this package. +type Enum uint32 + +// Types are defined a structs so that in debug mode they can carry +// extra information, such as a string name. See typesdebug.go. + +// Attrib identifies the location of a specific attribute variable. +type Attrib struct { + Value uint +} + +// Program identifies a compiled shader program. +type Program struct { + // Init is set by CreateProgram, as some GL drivers (in particular, + // ANGLE) return true for glIsProgram(0). + Init bool + Value uint32 +} + +// Shader identifies a GLSL shader. +type Shader struct { + Value uint32 +} + +// Buffer identifies a GL buffer object. +type Buffer struct { + Value uint32 +} + +// Framebuffer identifies a GL framebuffer. +type Framebuffer struct { + Value uint32 +} + +// A Renderbuffer is a GL object that holds an image in an internal format. +type Renderbuffer struct { + Value uint32 +} + +// A Texture identifies a GL texture unit. +type Texture struct { + Value uint32 +} + +// Uniform identifies the location of a specific uniform variable. +type Uniform struct { + Value int32 +} + +// A VertexArray is a GL object that holds vertices in an internal format. +type VertexArray struct { + Value uint32 +} + +func (v Attrib) c() uintptr { return uintptr(v.Value) } +func (v Enum) c() uintptr { return uintptr(v) } +func (v Program) c() uintptr { + if !v.Init { + ret := uintptr(0) + ret-- + return ret + } + return uintptr(v.Value) +} +func (v Shader) c() uintptr { return uintptr(v.Value) } +func (v Buffer) c() uintptr { return uintptr(v.Value) } +func (v Texture) c() uintptr { return uintptr(v.Value) } +func (v Uniform) c() uintptr { return uintptr(v.Value) } +func (v VertexArray) c() uintptr { return uintptr(v.Value) } + +func (v Attrib) String() string { return fmt.Sprintf("Attrib(%d)", v.Value) } +func (v Program) String() string { return fmt.Sprintf("Program(%d)", v.Value) } +func (v Shader) String() string { return fmt.Sprintf("Shader(%d)", v.Value) } +func (v Buffer) String() string { return fmt.Sprintf("Buffer(%d)", v.Value) } +func (v Framebuffer) String() string { return fmt.Sprintf("Framebuffer(%d)", v.Value) } +func (v Renderbuffer) String() string { return fmt.Sprintf("Renderbuffer(%d)", v.Value) } +func (v Texture) String() string { return fmt.Sprintf("Texture(%d)", v.Value) } +func (v Uniform) String() string { return fmt.Sprintf("Uniform(%d)", v.Value) } +func (v VertexArray) String() string { return fmt.Sprintf("VertexArray(%d)", v.Value) } diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/work.c b/vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/work.c new file mode 100644 index 0000000..79b6bd1 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/work.c @@ -0,0 +1,181 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build darwin || linux || openbsd || freebsd + +#include +#include "_cgo_export.h" +#include "work.h" + +#if defined(GL_ES_VERSION_3_0) && GL_ES_VERSION_3_0 +#else +#include +static void gles3missing() { + printf("GLES3 function is missing\n"); + exit(2); +} +static void glBindVertexArray(GLuint array) { gles3missing(); } +static void glGenVertexArrays(GLsizei n, GLuint *arrays) { gles3missing(); } +#endif + +uintptr_t processFn(struct fnargs* args, char* parg) { + uintptr_t ret = 0; + switch (args->fn) { + case glfnUNDEFINED: + abort(); // bad glfn + break; + case glfnActiveTexture: + glActiveTexture((GLenum)args->a0); + break; + case glfnAttachShader: + glAttachShader((GLint)args->a0, (GLint)args->a1); + break; + case glfnBindBuffer: + glBindBuffer((GLenum)args->a0, (GLuint)args->a1); + break; + case glfnBindTexture: + glBindTexture((GLenum)args->a0, (GLint)args->a1); + break; + case glfnBindVertexArray: + glBindVertexArray((GLenum)args->a0); + break; + case glfnBlendColor: + glBlendColor(*(GLfloat*)&args->a0, *(GLfloat*)&args->a1, *(GLfloat*)&args->a2, *(GLfloat*)&args->a3); + break; + case glfnBlendFunc: + glBlendFunc((GLenum)args->a0, (GLenum)args->a1); + break; + case glfnBufferData: + glBufferData((GLenum)args->a0, (GLsizeiptr)args->a1, (GLvoid*)parg, (GLenum)args->a2); + break; + case glfnBufferSubData: + glBufferSubData((GLenum)args->a0, (GLsizeiptr)args->a1, (GLenum)args->a2, (GLvoid*)parg); + break; + case glfnClear: + glClear((GLenum)args->a0); + break; + case glfnClearColor: + glClearColor(*(GLfloat*)&args->a0, *(GLfloat*)&args->a1, *(GLfloat*)&args->a2, *(GLfloat*)&args->a3); + break; + case glfnCompileShader: + glCompileShader((GLint)args->a0); + break; + case glfnCreateProgram: + ret = glCreateProgram(); + break; + case glfnCreateShader: + ret = glCreateShader((GLenum)args->a0); + break; + case glfnDeleteBuffer: + glDeleteBuffers(1, (const GLuint*)(&args->a0)); + break; + case glfnDeleteTexture: + glDeleteTextures(1, (const GLuint*)(&args->a0)); + break; + case glfnDisable: + glDisable((GLenum)args->a0); + break; + case glfnDrawArrays: + glDrawArrays((GLenum)args->a0, (GLint)args->a1, (GLint)args->a2); + break; + case glfnEnable: + glEnable((GLenum)args->a0); + break; + case glfnEnableVertexAttribArray: + glEnableVertexAttribArray((GLint)args->a0); + break; + case glfnFlush: + glFlush(); + break; + case glfnGenBuffer: + glGenBuffers(1, (GLuint*)&ret); + break; + case glfnGenTexture: + glGenTextures(1, (GLuint*)&ret); + break; + case glfnGenVertexArray: + glGenVertexArrays(1, (GLuint*)&ret); + break; + case glfnGetAttribLocation: + ret = glGetAttribLocation((GLint)args->a0, (GLchar*)args->a1); + break; + case glfnGetError: + ret = glGetError(); + break; + case glfnGetProgramiv: + glGetProgramiv((GLint)args->a0, (GLenum)args->a1, (GLint*)&ret); + break; + case glfnGetProgramInfoLog: + glGetProgramInfoLog((GLuint)args->a0, (GLsizei)args->a1, 0, (GLchar*)parg); + break; + case glfnGetShaderiv: + glGetShaderiv((GLint)args->a0, (GLenum)args->a1, (GLint*)&ret); + break; + case glfnGetShaderInfoLog: + glGetShaderInfoLog((GLuint)args->a0, (GLsizei)args->a1, 0, (GLchar*)parg); + break; + case glfnGetShaderSource: + glGetShaderSource((GLuint)args->a0, (GLsizei)args->a1, 0, (GLchar*)parg); + break; + case glfnGetTexParameteriv: + glGetTexParameteriv((GLenum)args->a0, (GLenum)args->a1, (GLint*)parg); + break; + case glfnGetUniformLocation: + ret = glGetUniformLocation((GLint)args->a0, (GLchar*)args->a1); + break; + case glfnLinkProgram: + glLinkProgram((GLint)args->a0); + break; + case glfnReadPixels: + glReadPixels((GLint)args->a0, (GLint)args->a1, (GLsizei)args->a2, (GLsizei)args->a3, (GLenum)args->a4, (GLenum)args->a5, (void*)parg); + break; + case glfnScissor: + glScissor((GLint)args->a0, (GLint)args->a1, (GLint)args->a2, (GLint)args->a3); + break; + case glfnShaderSource: +#if defined(os_ios) || defined(os_macos) + glShaderSource((GLuint)args->a0, (GLsizei)args->a1, (const GLchar *const *)args->a2, NULL); +#else + glShaderSource((GLuint)args->a0, (GLsizei)args->a1, (const GLchar **)args->a2, NULL); +#endif + break; + case glfnTexImage2D: + glTexImage2D( + (GLenum)args->a0, + (GLint)args->a1, + (GLint)args->a2, + (GLsizei)args->a3, + (GLsizei)args->a4, + 0, // border + (GLenum)args->a5, + (GLenum)args->a6, + (const GLvoid*)parg); + break; + case glfnTexParameteri: + glTexParameteri((GLenum)args->a0, (GLenum)args->a1, (GLint)args->a2); + break; + case glfnUniform1f: + glUniform1f((GLint)args->a0, *(GLfloat*)&args->a1); + break; + case glfnUniform2f: + glUniform2f((GLint)args->a0, *(GLfloat*)&args->a1, *(GLfloat*)&args->a2); + break; + case glfnUniform4f: + glUniform4f((GLint)args->a0, *(GLfloat*)&args->a1, *(GLfloat*)&args->a2, *(GLfloat*)&args->a3, *(GLfloat*)&args->a4); + break; + case glfnUniform4fv: + glUniform4fv((GLint)args->a0, (GLsizeiptr)args->a1, (GLvoid*)parg); + break; + case glfnUseProgram: + glUseProgram((GLint)args->a0); + break; + case glfnVertexAttribPointer: + glVertexAttribPointer((GLuint)args->a0, (GLint)args->a1, (GLenum)args->a2, (GLboolean)args->a3, (GLsizei)args->a4, (const GLvoid*)args->a5); + break; + case glfnViewport: + glViewport((GLint)args->a0, (GLint)args->a1, (GLint)args->a2, (GLint)args->a3); + break; + } + return ret; +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/work.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/work.go new file mode 100644 index 0000000..4640ce8 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/work.go @@ -0,0 +1,175 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build darwin || linux || openbsd || freebsd + +package gl + +/* +#cgo ios LDFLAGS: -framework OpenGLES +#cgo darwin,!ios LDFLAGS: -framework OpenGL +#cgo linux LDFLAGS: -lGLESv2 +#cgo openbsd LDFLAGS: -L/usr/X11R6/lib/ -lGLESv2 +#cgo freebsd LDFLAGS: -L/usr/local/lib/ -lGLESv2 + +#cgo android CFLAGS: -Dos_android +#cgo ios CFLAGS: -Dos_ios +#cgo darwin,!ios CFLAGS: -Dos_macos +#cgo darwin CFLAGS: -DGL_SILENCE_DEPRECATION +#cgo linux CFLAGS: -Dos_linux +#cgo openbsd CFLAGS: -Dos_openbsd +#cgo freebsd CFLAGS: -Dos_freebsd +#cgo openbsd CFLAGS: -I/usr/X11R6/include/ +#cgo freebsd CFLAGS: -I/usr/local/include/ + +#include +#include "work.h" + +uintptr_t process(struct fnargs* cargs, char* parg0, char* parg1, char* parg2, int count) { + uintptr_t ret; + + ret = processFn(&cargs[0], parg0); + if (count > 1) { + ret = processFn(&cargs[1], parg1); + } + if (count > 2) { + ret = processFn(&cargs[2], parg2); + } + + return ret; +} +*/ +import "C" + +import ( + "unsafe" + + "fyne.io/fyne/v2/internal/async" +) + +const workbufLen = 3 + +type context struct { + cptr uintptr + debug int32 + + workAvailable *async.UnboundedStructChan + + // work is a queue of calls to execute. + work chan call + + // retvalue is sent a return value when blocking calls complete. + // It is safe to use a global unbuffered channel here as calls + // cannot currently be made concurrently. + // + // TODO: the comment above about concurrent calls isn't actually true: package + // app calls package gl, but it has to do so in a separate goroutine, which + // means that its gl calls (which may be blocking) can race with other gl calls + // in the main program. We should make it safe to issue blocking gl calls + // concurrently, or get the gl calls out of package app, or both. + retvalue chan C.uintptr_t + + cargs [workbufLen]C.struct_fnargs + parg [workbufLen]*C.char +} + +func (ctx *context) WorkAvailable() <-chan struct{} { return ctx.workAvailable.Out() } + +type context3 struct { + *context +} + +// NewContext creates a cgo OpenGL context. +// +// See the Worker interface for more details on how it is used. +func NewContext() (Context, Worker) { + glctx := &context{ + workAvailable: async.NewUnboundedStructChan(), + work: make(chan call, workbufLen*4), + retvalue: make(chan C.uintptr_t), + } + if C.GLES_VERSION == "GL_ES_2_0" { + return glctx, glctx + } + return context3{glctx}, glctx +} + +// Version returns a GL ES version string, either "GL_ES_2_0" or "GL_ES_3_0". +// Future versions of the gl package may return "GL_ES_3_1". +func Version() string { + return C.GLES_VERSION +} + +func (ctx *context) enqueue(c call) uintptr { + ctx.work <- c + ctx.workAvailable.In() <- struct{}{} + + if c.blocking { + return uintptr(<-ctx.retvalue) + } + return 0 +} + +func (ctx *context) DoWork() { + queue := make([]call, 0, workbufLen) + for { + // Wait until at least one piece of work is ready. + // Accumulate work until a piece is marked as blocking. + select { + case w := <-ctx.work: + queue = append(queue, w) + default: + return + } + blocking := queue[len(queue)-1].blocking + enqueue: + for len(queue) < cap(queue) && !blocking { + select { + case w := <-ctx.work: + queue = append(queue, w) + blocking = queue[len(queue)-1].blocking + default: + break enqueue + } + } + + // Process the queued GL functions. + for i, q := range queue { + ctx.cargs[i] = *(*C.struct_fnargs)(unsafe.Pointer(&q.args)) + ctx.parg[i] = (*C.char)(q.parg) + } + ret := C.process(&ctx.cargs[0], ctx.parg[0], ctx.parg[1], ctx.parg[2], C.int(len(queue))) + + // Cleanup and signal. + queue = queue[:0] + if blocking { + ctx.retvalue <- ret + } + } +} + +// If C.GLint is not int32, one of these will cause a division by zero compile error. +const ( + _ = 1 / int(unsafe.Sizeof(C.GLint(0))/unsafe.Sizeof(int32(0))) + _ = 1 / int(unsafe.Sizeof(int32(0))/unsafe.Sizeof(C.GLint(0))) +) + +// cString creates C string off the Go heap. +// ret is a *char. +func (ctx *context) cString(str string) (uintptr, func()) { + ptr := unsafe.Pointer(C.CString(str)) + return uintptr(ptr), func() { C.free(ptr) } +} + +// cString creates a pointer to a C string off the Go heap. +// ret is a **char. +func (ctx *context) cStringPtr(str string) (uintptr, func()) { + s, free := ctx.cString(str) + ptr := C.malloc(C.size_t(unsafe.Sizeof((*int)(nil)))) + *(*uintptr)(ptr) = s + return uintptr(ptr), func() { + free() + C.free(ptr) + } +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/work.h b/vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/work.h new file mode 100644 index 0000000..dd95434 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/work.h @@ -0,0 +1,104 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifdef os_android +// TODO(crawshaw): We could include and +// condition on __ANDROID_API__ to get GLES3 headers. However +// we also need to add -lGLESv3 to LDFLAGS, which we cannot do +// from inside an ifdef. +#include +#elif os_linux +#include // install on Ubuntu with: sudo apt-get install libegl1-mesa-dev libgles2-mesa-dev libx11-dev +#elif os_openbsd +#include +#elif os_freebsd +#include +#endif + +#ifdef os_ios +#include +#endif + +#ifdef os_macos +#include +#define GL_ES_VERSION_3_0 1 +#endif + +#if defined(GL_ES_VERSION_3_0) && GL_ES_VERSION_3_0 +#define GLES_VERSION "GL_ES_3_0" +#else +#define GLES_VERSION "GL_ES_2_0" +#endif + +#include +#include + +// TODO: generate this enum from fn.go. +typedef enum { + glfnUNDEFINED, + glfnActiveTexture, + glfnAttachShader, + glfnBindBuffer, + glfnBindTexture, + glfnBindVertexArray, + glfnBlendColor, + glfnBlendFunc, + glfnBufferData, + glfnBufferSubData, + glfnClear, + glfnClearColor, + glfnCompileShader, + glfnCreateProgram, + glfnCreateShader, + glfnDeleteBuffer, + glfnDeleteTexture, + glfnDisable, + glfnDrawArrays, + glfnEnable, + glfnEnableVertexAttribArray, + glfnFlush, + glfnGenBuffer, + glfnGenTexture, + glfnGenVertexArray, + glfnGetAttribLocation, + glfnGetError, + glfnGetProgramInfoLog, + glfnGetProgramiv, + glfnGetShaderInfoLog, + glfnGetShaderSource, + glfnGetShaderiv, + glfnGetTexParameteriv, + glfnGetUniformLocation, + glfnLinkProgram, + glfnReadPixels, + glfnScissor, + glfnShaderSource, + glfnTexImage2D, + glfnTexParameteri, + glfnUniform1f, + glfnUniform2f, + glfnUniform4f, + glfnUniform4fv, + glfnUseProgram, + glfnVertexAttribPointer, + glfnViewport, +} glfn; + +// TODO: generate this type from fn.go. +struct fnargs { + glfn fn; + + uintptr_t a0; + uintptr_t a1; + uintptr_t a2; + uintptr_t a3; + uintptr_t a4; + uintptr_t a5; + uintptr_t a6; + uintptr_t a7; + uintptr_t a8; + uintptr_t a9; +}; + +extern uintptr_t processFn(struct fnargs* args, char* parg); diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/work_other.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/work_other.go new file mode 100644 index 0000000..5168f0e --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/work_other.go @@ -0,0 +1,35 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build (!cgo || (!darwin && !linux && !openbsd && !freebsd)) && !windows + +package gl + +// This file contains stub implementations of what the other work*.go files +// provide. These stubs don't do anything, other than compile (e.g. when cgo is +// disabled). + +type context struct{} + +func (*context) enqueue(c call) uintptr { + panic("unimplemented; GOOS/CGO combination not supported") +} + +func (*context) cString(str string) (uintptr, func()) { + panic("unimplemented; GOOS/CGO combination not supported") +} + +func (*context) cStringPtr(str string) (uintptr, func()) { + panic("unimplemented; GOOS/CGO combination not supported") +} + +type context3 = context + +func NewContext() (Context, Worker) { + panic("unimplemented; GOOS/CGO combination not supported") +} + +func Version() string { + panic("unimplemented; GOOS/CGO combination not supported") +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/work_windows.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/work_windows.go new file mode 100644 index 0000000..226356b --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/gl/work_windows.go @@ -0,0 +1,349 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package gl + +import ( + "syscall" + "unsafe" +) + +// context is described in work.go. +type context struct { + debug int32 + workAvailable chan struct{} + work chan call + retvalue chan uintptr + + // TODO(crawshaw): will not work with a moving collector + cStringCounter int + cStrings map[int]unsafe.Pointer +} + +func (ctx *context) WorkAvailable() <-chan struct{} { return ctx.workAvailable } + +type context3 struct { + *context +} + +func NewContext() (Context, Worker) { + if err := findDLLs(); err != nil { + panic(err) + } + glctx := &context{ + workAvailable: make(chan struct{}, 1), + work: make(chan call, 3), + retvalue: make(chan uintptr), + cStrings: make(map[int]unsafe.Pointer), + } + return glctx, glctx +} + +func (ctx *context) enqueue(c call) uintptr { + ctx.work <- c + + select { + case ctx.workAvailable <- struct{}{}: + default: + } + + if c.blocking { + return <-ctx.retvalue + } + return 0 +} + +func (ctx *context) DoWork() { + // TODO: add a work queue + for { + select { + case w := <-ctx.work: + ret := ctx.doWork(w) + if w.blocking { + ctx.retvalue <- ret + } + default: + return + } + } +} + +func (ctx *context) cString(s string) (uintptr, func()) { + buf := make([]byte, len(s)+1) + for i := 0; i < len(s); i++ { + buf[i] = s[i] + } + ret := unsafe.Pointer(&buf[0]) + id := ctx.cStringCounter + ctx.cStringCounter++ + ctx.cStrings[id] = ret + return uintptr(ret), func() { delete(ctx.cStrings, id) } +} + +func (ctx *context) cStringPtr(str string) (uintptr, func()) { + s, sfree := ctx.cString(str) + sptr := [2]uintptr{s, 0} + ret := unsafe.Pointer(&sptr[0]) + id := ctx.cStringCounter + ctx.cStringCounter++ + ctx.cStrings[id] = ret + return uintptr(ret), func() { sfree(); delete(ctx.cStrings, id) } +} + +var glfnFuncs = [...]func(c call) (ret uintptr){ + glfnActiveTexture: func(c call) (ret uintptr) { + syscall.SyscallN(glActiveTexture.Addr(), c.args.a0) + return ret + }, + glfnAttachShader: func(c call) (ret uintptr) { + syscall.SyscallN(glAttachShader.Addr(), c.args.a0, c.args.a1) + return ret + }, + glfnBindBuffer: func(c call) (ret uintptr) { + syscall.SyscallN(glBindBuffer.Addr(), c.args.a0, c.args.a1) + return ret + }, + glfnBindTexture: func(c call) (ret uintptr) { + syscall.SyscallN(glBindTexture.Addr(), c.args.a0, c.args.a1) + return ret + }, + glfnBindVertexArray: func(c call) (ret uintptr) { + syscall.SyscallN(glBindVertexArray.Addr(), c.args.a0) + return ret + }, + glfnBlendColor: func(c call) (ret uintptr) { + syscall.SyscallN(glBlendColor.Addr(), c.args.a0, c.args.a1, c.args.a2, c.args.a3) + return ret + }, + glfnBlendFunc: func(c call) (ret uintptr) { + syscall.SyscallN(glBlendFunc.Addr(), c.args.a0, c.args.a1) + return ret + }, + glfnBufferData: func(c call) (ret uintptr) { + syscall.SyscallN(glBufferData.Addr(), c.args.a0, c.args.a1, uintptr(c.parg), c.args.a2) + return ret + }, + glfnBufferSubData: func(c call) (ret uintptr) { + syscall.SyscallN(glBufferSubData.Addr(), c.args.a0, c.args.a1, c.args.a2, uintptr(c.parg)) + return ret + }, + glfnClear: func(c call) (ret uintptr) { + syscall.SyscallN(glClear.Addr(), c.args.a0) + return ret + }, + glfnClearColor: func(c call) (ret uintptr) { + syscall.SyscallN(glClearColor.Addr(), c.args.a0, c.args.a1, c.args.a2, c.args.a3) + return ret + }, + glfnCompileShader: func(c call) (ret uintptr) { + syscall.SyscallN(glCompileShader.Addr(), c.args.a0) + return ret + }, + glfnCreateProgram: func(c call) (ret uintptr) { + ret, _, _ = syscall.SyscallN(glCreateProgram.Addr()) + return ret + }, + glfnCreateShader: func(c call) (ret uintptr) { + ret, _, _ = syscall.SyscallN(glCreateShader.Addr(), c.args.a0) + return ret + }, + glfnDeleteBuffer: func(c call) (ret uintptr) { + syscall.SyscallN(glDeleteBuffers.Addr(), 1, uintptr(unsafe.Pointer(&c.args.a0))) + return ret + }, + glfnDeleteTexture: func(c call) (ret uintptr) { + syscall.SyscallN(glDeleteTextures.Addr(), 1, uintptr(unsafe.Pointer(&c.args.a0))) + return ret + }, + glfnDisable: func(c call) (ret uintptr) { + syscall.SyscallN(glDisable.Addr(), c.args.a0) + return ret + }, + glfnDrawArrays: func(c call) (ret uintptr) { + syscall.SyscallN(glDrawArrays.Addr(), c.args.a0, c.args.a1, c.args.a2) + return ret + }, + glfnEnable: func(c call) (ret uintptr) { + syscall.SyscallN(glEnable.Addr(), c.args.a0) + return ret + }, + glfnEnableVertexAttribArray: func(c call) (ret uintptr) { + syscall.SyscallN(glEnableVertexAttribArray.Addr(), c.args.a0) + return ret + }, + glfnFlush: func(c call) (ret uintptr) { + syscall.SyscallN(glFlush.Addr()) + return ret + }, + glfnGenBuffer: func(c call) (ret uintptr) { + syscall.SyscallN(glGenBuffers.Addr(), 1, uintptr(unsafe.Pointer(&ret))) + return ret + }, + glfnGenVertexArray: func(c call) (ret uintptr) { + syscall.SyscallN(glGenVertexArrays.Addr(), 1, uintptr(unsafe.Pointer(&ret))) + return ret + }, + glfnGenTexture: func(c call) (ret uintptr) { + syscall.SyscallN(glGenTextures.Addr(), 1, uintptr(unsafe.Pointer(&ret))) + return ret + }, + glfnGetAttribLocation: func(c call) (ret uintptr) { + ret, _, _ = syscall.SyscallN(glGetAttribLocation.Addr(), c.args.a0, c.args.a1) + return ret + }, + glfnGetError: func(c call) (ret uintptr) { + ret, _, _ = syscall.SyscallN(glGetError.Addr()) + return ret + }, + glfnGetProgramInfoLog: func(c call) (ret uintptr) { + syscall.SyscallN(glGetProgramInfoLog.Addr(), c.args.a0, c.args.a1, 0, uintptr(c.parg)) + return ret + }, + glfnGetProgramiv: func(c call) (ret uintptr) { + syscall.SyscallN(glGetProgramiv.Addr(), c.args.a0, c.args.a1, uintptr(unsafe.Pointer(&ret))) + return ret + }, + glfnGetShaderInfoLog: func(c call) (ret uintptr) { + syscall.SyscallN(glGetShaderInfoLog.Addr(), c.args.a0, c.args.a1, 0, uintptr(c.parg)) + return ret + }, + glfnGetShaderSource: func(c call) (ret uintptr) { + syscall.SyscallN(glGetShaderSource.Addr(), c.args.a0, c.args.a1, 0, uintptr(c.parg)) + return ret + }, + glfnGetShaderiv: func(c call) (ret uintptr) { + syscall.SyscallN(glGetShaderiv.Addr(), c.args.a0, c.args.a1, uintptr(unsafe.Pointer(&ret))) + return ret + }, + glfnGetTexParameteriv: func(c call) (ret uintptr) { + syscall.SyscallN(glGetTexParameteriv.Addr(), c.args.a0, c.args.a1, uintptr(c.parg)) + return ret + }, + glfnGetUniformLocation: func(c call) (ret uintptr) { + ret, _, _ = syscall.SyscallN(glGetUniformLocation.Addr(), c.args.a0, c.args.a1) + return ret + }, + glfnLinkProgram: func(c call) (ret uintptr) { + syscall.SyscallN(glLinkProgram.Addr(), c.args.a0) + return ret + }, + glfnReadPixels: func(c call) (ret uintptr) { + syscall.SyscallN(glReadPixels.Addr(), c.args.a0, c.args.a1, c.args.a2, c.args.a3, c.args.a4, c.args.a5, uintptr(c.parg)) + return ret + }, + glfnScissor: func(c call) (ret uintptr) { + syscall.SyscallN(glScissor.Addr(), c.args.a0, c.args.a1, c.args.a2, c.args.a3) + return ret + }, + glfnShaderSource: func(c call) (ret uintptr) { + syscall.SyscallN(glShaderSource.Addr(), c.args.a0, c.args.a1, c.args.a2, 0) + return ret + }, + glfnTexImage2D: func(c call) (ret uintptr) { + syscall.SyscallN(glTexImage2D.Addr(), c.args.a0, c.args.a1, c.args.a2, c.args.a3, c.args.a4, 0, c.args.a5, c.args.a6, uintptr(c.parg)) + return ret + }, + glfnTexParameteri: func(c call) (ret uintptr) { + syscall.SyscallN(glTexParameteri.Addr(), c.args.a0, c.args.a1, c.args.a2) + return ret + }, + glfnUniform1f: func(c call) (ret uintptr) { + syscall.SyscallN(glUniform1f.Addr(), c.args.a0, c.args.a1) + return ret + }, + glfnUniform2f: func(c call) (ret uintptr) { + syscall.SyscallN(glUniform2f.Addr(), c.args.a0, c.args.a1, c.args.a2) + return ret + }, + glfnUniform4f: func(c call) (ret uintptr) { + syscall.SyscallN(glUniform4f.Addr(), c.args.a0, c.args.a1, c.args.a2, c.args.a3, c.args.a4) + return ret + }, + glfnUniform4fv: func(c call) (ret uintptr) { + syscall.SyscallN(glUniform4fv.Addr(), c.args.a0, c.args.a1, uintptr(c.parg)) + return ret + }, + glfnUseProgram: func(c call) (ret uintptr) { + syscall.SyscallN(glUseProgram.Addr(), c.args.a0) + return ret + }, + glfnVertexAttribPointer: func(c call) (ret uintptr) { + syscall.SyscallN(glVertexAttribPointer.Addr(), c.args.a0, c.args.a1, c.args.a2, c.args.a3, c.args.a4, c.args.a5) + return ret + }, + glfnViewport: func(c call) (ret uintptr) { + syscall.SyscallN(glViewport.Addr(), c.args.a0, c.args.a1, c.args.a2, c.args.a3) + return ret + }, +} + +func (ctx *context) doWork(c call) (ret uintptr) { + if int(c.args.fn) < len(glfnFuncs) { + return glfnFuncs[c.args.fn](c) + } + panic("unknown GL function") +} + +// Exported libraries for a Windows GUI driver. +// +// LibEGL is not used directly by the gl package, but is needed by any +// driver hoping to use OpenGL ES. +// +// LibD3DCompiler is needed by libglesv2.dll for compiling shaders. +var ( + LibGLESv2 = syscall.NewLazyDLL("libglesv2.dll") + LibEGL = syscall.NewLazyDLL("libegl.dll") + LibD3DCompiler = syscall.NewLazyDLL("d3dcompiler_47.dll") +) + +var ( + libGLESv2 = LibGLESv2 + glActiveTexture = libGLESv2.NewProc("glActiveTexture") + glAttachShader = libGLESv2.NewProc("glAttachShader") + glBindBuffer = libGLESv2.NewProc("glBindBuffer") + glBindTexture = libGLESv2.NewProc("glBindTexture") + glBindVertexArray = libGLESv2.NewProc("glBindVertexArray") + glBlendColor = libGLESv2.NewProc("glBlendColor") + glBlendFunc = libGLESv2.NewProc("glBlendFunc") + glBufferData = libGLESv2.NewProc("glBufferData") + glBufferSubData = libGLESv2.NewProc("glBufferSubData") + glClear = libGLESv2.NewProc("glClear") + glClearColor = libGLESv2.NewProc("glClearColor") + glCompileShader = libGLESv2.NewProc("glCompileShader") + glCreateProgram = libGLESv2.NewProc("glCreateProgram") + glCreateShader = libGLESv2.NewProc("glCreateShader") + glDeleteBuffers = libGLESv2.NewProc("glDeleteBuffers") + glDeleteTextures = libGLESv2.NewProc("glDeleteTextures") + glDisable = libGLESv2.NewProc("glDisable") + glDrawArrays = libGLESv2.NewProc("glDrawArrays") + glEnable = libGLESv2.NewProc("glEnable") + glEnableVertexAttribArray = libGLESv2.NewProc("glEnableVertexAttribArray") + glFlush = libGLESv2.NewProc("glFlush") + glGenBuffers = libGLESv2.NewProc("glGenBuffers") + glGenTextures = libGLESv2.NewProc("glGenTextures") + glGenVertexArrays = libGLESv2.NewProc("glGenVertexArrays") + glGetAttribLocation = libGLESv2.NewProc("glGetAttribLocation") + glGetError = libGLESv2.NewProc("glGetError") + glGetProgramInfoLog = libGLESv2.NewProc("glGetProgramInfoLog") + glGetProgramiv = libGLESv2.NewProc("glGetProgramiv") + glGetShaderInfoLog = libGLESv2.NewProc("glGetShaderInfoLog") + glGetShaderSource = libGLESv2.NewProc("glGetShaderSource") + glGetShaderiv = libGLESv2.NewProc("glGetShaderiv") + glGetTexParameteriv = libGLESv2.NewProc("glGetTexParameteriv") + glGetUniformLocation = libGLESv2.NewProc("glGetUniformLocation") + glPixelStorei = libGLESv2.NewProc("glPixelStorei") + glLinkProgram = libGLESv2.NewProc("glLinkProgram") + glReadPixels = libGLESv2.NewProc("glReadPixels") + glScissor = libGLESv2.NewProc("glScissor") + glShaderSource = libGLESv2.NewProc("glShaderSource") + glTexImage2D = libGLESv2.NewProc("glTexImage2D") + glTexParameteri = libGLESv2.NewProc("glTexParameteri") + glUniform1f = libGLESv2.NewProc("glUniform1f") + glUniform2f = libGLESv2.NewProc("glUniform2f") + glUniform4f = libGLESv2.NewProc("glUniform4f") + glUniform4fv = libGLESv2.NewProc("glUniform4fv") + glUseProgram = libGLESv2.NewProc("glUseProgram") + glVertexAttribPointer = libGLESv2.NewProc("glVertexAttribPointer") + glViewport = libGLESv2.NewProc("glViewport") +) diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/keyboard.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/keyboard.go new file mode 100644 index 0000000..35c4d67 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/keyboard.go @@ -0,0 +1,46 @@ +package mobile + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/driver/mobile" + "fyne.io/fyne/v2/internal/driver/mobile/app" +) + +func (d *device) hideVirtualKeyboard() { + if drv, ok := fyne.CurrentApp().Driver().(*driver); ok { + if drv.app == nil { // not yet running + return + } + + drv.app.HideVirtualKeyboard() + d.keyboardShown = false + } +} + +func (d *device) handleKeyboard(obj fyne.Focusable) { + isDisabled := false + if disWid, ok := obj.(fyne.Disableable); ok { + isDisabled = disWid.Disabled() + } + if obj != nil && !isDisabled { + if keyb, ok := obj.(mobile.Keyboardable); ok { + d.showVirtualKeyboard(keyb.Keyboard()) + } else { + d.showVirtualKeyboard(mobile.DefaultKeyboard) + } + } else { + d.hideVirtualKeyboard() + } +} + +func (d *device) showVirtualKeyboard(keyboard mobile.KeyboardType) { + if drv, ok := fyne.CurrentApp().Driver().(*driver); ok { + if drv.app == nil { // not yet running + fyne.LogError("Cannot show keyboard before app is running", nil) + return + } + + d.keyboardShown = true + drv.app.ShowVirtualKeyboard(app.KeyboardType(keyboard)) + } +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/menu.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/menu.go new file mode 100644 index 0000000..cc446c9 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/menu.go @@ -0,0 +1,125 @@ +package mobile + +import ( + "image/color" + + "fyne.io/fyne/v2" + fynecanvas "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" +) + +type menuLabel struct { + widget.BaseWidget + + menu *fyne.Menu + bar *fyne.Container + canvas *canvas +} + +func (m *menuLabel) Tapped(*fyne.PointEvent) { + pos := fyne.CurrentApp().Driver().AbsolutePositionForObject(m) + menu := widget.NewPopUpMenu(m.menu, m.canvas) + menu.ShowAtPosition(fyne.NewPos(pos.X+m.Size().Width, pos.Y)) + + menuDismiss := menu.OnDismiss // this dismisses the menu stack + menu.OnDismiss = func() { + menuDismiss() + m.bar.Hide() // dismiss the overlay menu bar + m.canvas.setMenu(nil) + } +} + +func (m *menuLabel) CreateRenderer() fyne.WidgetRenderer { + label := widget.NewLabel(m.menu.Label) + box := container.NewHBox(layout.NewSpacer(), label, layout.NewSpacer(), widget.NewIcon(theme.MenuExpandIcon())) + + return &menuLabelRenderer{menu: m, content: box} +} + +func newMenuLabel(item *fyne.Menu, parent *fyne.Container, c *canvas) *menuLabel { + l := &menuLabel{menu: item, bar: parent, canvas: c} + l.ExtendBaseWidget(l) + return l +} + +func (c *canvas) showMenu(menu *fyne.MainMenu) { + var panel *fyne.Container + top := container.NewHBox(widget.NewButtonWithIcon("", theme.CancelIcon(), func() { + panel.Hide() + c.setMenu(nil) + })) + panel = container.NewVBox(top) + for _, item := range menu.Items { + panel.Add(newMenuLabel(item, panel, c)) + } + if c.padded { + panel = container.NewPadded(panel) + } + + bg := fynecanvas.NewRectangle(theme.Color(theme.ColorNameBackground)) + shadow := fynecanvas.NewHorizontalGradient(theme.Color(theme.ColorNameShadow), color.Transparent) + + safePos, safeSize := c.InteractiveArea() + bg.Move(safePos) + bg.Resize(fyne.NewSize(panel.MinSize().Width+theme.Padding(), safeSize.Height)) + panel.Move(safePos) + panel.Resize(fyne.NewSize(panel.MinSize().Width+theme.Padding(), safeSize.Height)) + shadow.Resize(fyne.NewSize(theme.Padding()/2, safeSize.Height)) + shadow.Move(fyne.NewPos(panel.Size().Width+safePos.X, safePos.Y)) + + c.setMenu(container.NewWithoutLayout(bg, panel, shadow)) +} + +func (d *driver) findMenu(win *window) *fyne.MainMenu { + if win.menu != nil { + return win.menu + } + + matched := false + for x := len(d.windows) - 1; x >= 0; x-- { + w := d.windows[x] + if !matched { + if w == win { + matched = true + } + continue + } + + if w.(*window).menu != nil { + return w.(*window).menu + } + } + + return nil +} + +type menuLabelRenderer struct { + menu *menuLabel + content *fyne.Container +} + +func (m *menuLabelRenderer) BackgroundColor() color.Color { + return theme.Color(theme.ColorNameBackground) +} + +func (m *menuLabelRenderer) Destroy() { +} + +func (m *menuLabelRenderer) Layout(size fyne.Size) { + m.content.Resize(size) +} + +func (m *menuLabelRenderer) MinSize() fyne.Size { + return m.content.MinSize() +} + +func (m *menuLabelRenderer) Objects() []fyne.CanvasObject { + return []fyne.CanvasObject{m.content} +} + +func (m *menuLabelRenderer) Refresh() { + m.content.Refresh() +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/menubutton.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/menubutton.go new file mode 100644 index 0000000..923f76e --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/menubutton.go @@ -0,0 +1,52 @@ +package mobile + +import ( + "fyne.io/fyne/v2" + fynecanvas "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" +) + +type menuButton struct { + widget.BaseWidget + win *window + menu *fyne.MainMenu +} + +func (w *window) newMenuButton(menu *fyne.MainMenu) *menuButton { + b := &menuButton{win: w, menu: menu} + b.ExtendBaseWidget(b) + return b +} + +func (m *menuButton) CreateRenderer() fyne.WidgetRenderer { + return &menuButtonRenderer{btn: widget.NewButtonWithIcon("", theme.MenuIcon(), func() { + m.win.canvas.showMenu(m.menu) + }), bg: fynecanvas.NewRectangle(theme.Color(theme.ColorNameBackground))} +} + +type menuButtonRenderer struct { + btn *widget.Button + bg *fynecanvas.Rectangle +} + +func (m *menuButtonRenderer) Destroy() { +} + +func (m *menuButtonRenderer) Layout(size fyne.Size) { + m.bg.Move(fyne.NewPos(theme.Padding()/2, theme.Padding()/2)) + m.bg.Resize(size.Subtract(fyne.NewSize(theme.Padding(), theme.Padding()))) + m.btn.Resize(size) +} + +func (m *menuButtonRenderer) MinSize() fyne.Size { + return m.btn.MinSize() +} + +func (m *menuButtonRenderer) Objects() []fyne.CanvasObject { + return []fyne.CanvasObject{m.bg, m.btn} +} + +func (m *menuButtonRenderer) Refresh() { + m.bg.FillColor = theme.Color(theme.ColorNameBackground) +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/mobileinit/ctx_android.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/mobileinit/ctx_android.go new file mode 100644 index 0000000..562cc38 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/mobileinit/ctx_android.go @@ -0,0 +1,135 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package mobileinit + +/* +#include +#include + +static char* lockJNI(JavaVM *vm, uintptr_t* envp, int* attachedp) { + JNIEnv* env; + + if (vm == NULL) { + return "no current JVM"; + } + + *attachedp = 0; + switch ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6)) { + case JNI_OK: + break; + case JNI_EDETACHED: + if ((*vm)->AttachCurrentThread(vm, &env, 0) != 0) { + return "cannot attach to JVM"; + } + *attachedp = 1; + break; + case JNI_EVERSION: + return "bad JNI version"; + default: + return "unknown JNI error from GetEnv"; + } + + *envp = (uintptr_t)env; + return NULL; +} + +static char* checkException(uintptr_t jnienv) { + jthrowable exc; + JNIEnv* env = (JNIEnv*)jnienv; + + if (!(*env)->ExceptionCheck(env)) { + return NULL; + } + + exc = (*env)->ExceptionOccurred(env); + (*env)->ExceptionClear(env); + + jclass clazz = (*env)->FindClass(env, "java/lang/Throwable"); + jmethodID toString = (*env)->GetMethodID(env, clazz, "toString", "()Ljava/lang/String;"); + jobject msgStr = (*env)->CallObjectMethod(env, exc, toString); + return (char*)(*env)->GetStringUTFChars(env, msgStr, 0); +} + +static void unlockJNI(JavaVM *vm) { + (*vm)->DetachCurrentThread(vm); +} + +static void deletePrevCtx(JNIEnv* env,jobject ctx){ + if (ctx == NULL) { return; } + (*env)->DeleteGlobalRef(env, ctx); +} +*/ +import "C" + +import ( + "errors" + "runtime" + "unsafe" +) + +// currentVM is stored to initialize other cgo packages. +// +// As all the Go packages in a program form a single shared library, +// there can only be one JNI_OnLoad function for initialization. In +// OpenJDK there is JNI_GetCreatedJavaVMs, but this is not available +// on android. +var currentVM *C.JavaVM + +// currentCtx is Android's android.context.Context. May be NULL. +var currentCtx C.jobject + +// SetCurrentContext populates the global Context object with the specified +// current JavaVM instance (vm) and android.context.Context object (ctx). +// The android.context.Context object must be a global reference. +func SetCurrentContext(vm unsafe.Pointer, ctx uintptr) { + currentVM = (*C.JavaVM)(vm) + currentCtxPrev := currentCtx + currentCtx = (C.jobject)(ctx) + RunOnJVM(func(vm, jniEnv, ctx uintptr) error { + env := (*C.JNIEnv)(unsafe.Pointer(jniEnv)) + C.deletePrevCtx(env, C.jobject(currentCtxPrev)) + return nil + }) +} + +// RunOnJVM runs fn on a new goroutine locked to an OS thread with a JNIEnv. +// +// RunOnJVM blocks until the call to fn is complete. Any Java +// exception or failure to attach to the JVM is returned as an error. +// +// The function fn takes vm, the current JavaVM*, +// env, the current JNIEnv*, and +// ctx, a jobject representing the global android.context.Context. +func RunOnJVM(fn func(vm, env, ctx uintptr) error) error { + errch := make(chan error) + go func() { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + env := C.uintptr_t(0) + attached := C.int(0) + if errStr := C.lockJNI(currentVM, &env, &attached); errStr != nil { + errch <- errors.New(C.GoString(errStr)) + return + } + if attached != 0 { + defer C.unlockJNI(currentVM) + } + + vm := uintptr(unsafe.Pointer(currentVM)) + if err := fn(vm, uintptr(env), uintptr(currentCtx)); err != nil { + errch <- err + return + } + + if exc := C.checkException(env); exc != nil { + errch <- errors.New(C.GoString(exc)) + C.free(unsafe.Pointer(exc)) + return + } + errch <- nil + }() + return <-errch +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/mobileinit/mobileinit.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/mobileinit/mobileinit.go new file mode 100644 index 0000000..65c0912 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/mobileinit/mobileinit.go @@ -0,0 +1,11 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package mobileinit contains common initialization logic for mobile platforms +// that is relevant to both all-Go apps and gobind-based apps. +// +// Long-term, some code in this package should consider moving into Go stdlib. +package mobileinit + +import "C" diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/mobileinit/mobileinit_android.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/mobileinit/mobileinit_android.go new file mode 100644 index 0000000..fec5a16 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/mobileinit/mobileinit_android.go @@ -0,0 +1,93 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package mobileinit + +/* +To view the log output run: +adb logcat Fyne:I *:S +*/ + +// Android redirects stdout and stderr to /dev/null. +// As these are common debugging utilities in Go, +// we redirect them to logcat. +// +// Unfortunately, logcat is line oriented, so we must buffer. + +/* +#cgo LDFLAGS: -landroid -llog + +#include +#include +#include +*/ +import "C" + +import ( + "bufio" + "log" + "os" + "syscall" + "unsafe" +) + +var ( + ctag = C.CString("Fyne") + // Store the writer end of the redirected stderr and stdout + // so that they are not garbage collected and closed. + stderr, stdout *os.File +) + +type infoWriter struct{} + +func (infoWriter) Write(p []byte) (n int, err error) { + cstr := C.CString(string(p)) + C.__android_log_write(C.ANDROID_LOG_INFO, ctag, cstr) + C.free(unsafe.Pointer(cstr)) + return len(p), nil +} + +func lineLog(f *os.File, priority C.int) { + const logSize = 1024 // matches android/log.h. + r := bufio.NewReaderSize(f, logSize) + for { + line, _, err := r.ReadLine() + str := string(line) + if err != nil { + str += " " + err.Error() + } + cstr := C.CString(str) + C.__android_log_write(priority, ctag, cstr) + C.free(unsafe.Pointer(cstr)) + if err != nil { + break + } + } +} + +func init() { + log.SetOutput(infoWriter{}) + // android logcat includes all of log.LstdFlags + log.SetFlags(log.Flags() &^ log.LstdFlags) + + r, w, err := os.Pipe() + if err != nil { + panic(err) + } + stderr = w + if err := syscall.Dup3(int(w.Fd()), int(os.Stderr.Fd()), 0); err != nil { + panic(err) + } + go lineLog(r, C.ANDROID_LOG_ERROR) + + r, w, err = os.Pipe() + if err != nil { + panic(err) + } + stdout = w + if err := syscall.Dup3(int(w.Fd()), int(os.Stdout.Fd()), 0); err != nil { + panic(err) + } + go lineLog(r, C.ANDROID_LOG_INFO) +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/mobileinit/mobileinit_ios.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/mobileinit/mobileinit_ios.go new file mode 100644 index 0000000..ae33c12 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/mobileinit/mobileinit_ios.go @@ -0,0 +1,36 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build darwin && (arm || arm64) + +package mobileinit + +import ( + "log" + "unsafe" +) + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Foundation + +#include +#include + +void log_wrap(const char *logStr); +*/ +import "C" + +type aslWriter struct{} + +func (aslWriter) Write(p []byte) (n int, err error) { + cstr := C.CString(string(p)) + C.log_wrap(cstr) + C.free(unsafe.Pointer(cstr)) + return len(p), nil +} + +func init() { + log.SetOutput(aslWriter{}) +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/mobileinit/mobileinit_ios.m b/vendor/fyne.io/fyne/v2/internal/driver/mobile/mobileinit/mobileinit_ios.m new file mode 100644 index 0000000..84ab98d --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/mobileinit/mobileinit_ios.m @@ -0,0 +1,7 @@ +//go:build darwin && (arm || arm64) + +#import + +void log_wrap(const char *logStr) { + NSLog(@"Fyne: %s", logStr); +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/mobileinit/mobileinit_linux.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/mobileinit/mobileinit_linux.go new file mode 100644 index 0000000..f43aec2 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/mobileinit/mobileinit_linux.go @@ -0,0 +1 @@ +package mobileinit diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/repository.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/repository.go new file mode 100644 index 0000000..f115c0e --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/repository.go @@ -0,0 +1,77 @@ +package mobile + +import ( + "fyne.io/fyne/v2" + + "fyne.io/fyne/v2/storage/repository" +) + +// declare conformance with repository types +var ( + _ repository.Repository = (*mobileFileRepo)(nil) + _ repository.HierarchicalRepository = (*mobileFileRepo)(nil) + _ repository.ListableRepository = (*mobileFileRepo)(nil) + _ repository.WritableRepository = (*mobileFileRepo)(nil) + _ repository.AppendableRepository = (*mobileFileRepo)(nil) +) + +type mobileFileRepo struct{} + +func (m *mobileFileRepo) CanList(u fyne.URI) (bool, error) { + return canListURI(u), nil +} + +func (m *mobileFileRepo) CanRead(u fyne.URI) (bool, error) { + return true, nil // TODO check a file can be read +} + +func (m *mobileFileRepo) CanWrite(u fyne.URI) (bool, error) { + return true, nil // TODO check a file can be written +} + +func (m *mobileFileRepo) Child(u fyne.URI, name string) (fyne.URI, error) { + if u == nil || u.Scheme() != "file" { + return nil, repository.ErrOperationNotSupported + } + + return repository.GenericChild(u, name) +} + +func (m *mobileFileRepo) CreateListable(u fyne.URI) error { + return createListableURI(u) +} + +func (m *mobileFileRepo) Delete(u fyne.URI) error { + return deleteURI(u) +} + +func (m *mobileFileRepo) Destroy(string) { +} + +func (m *mobileFileRepo) Exists(u fyne.URI) (bool, error) { + return existsURI(u) +} + +func (m *mobileFileRepo) List(u fyne.URI) ([]fyne.URI, error) { + return listURI(u) +} + +func (m *mobileFileRepo) Parent(u fyne.URI) (fyne.URI, error) { + if u == nil || u.Scheme() != "file" { + return nil, repository.ErrOperationNotSupported + } + + return repository.GenericParent(u) +} + +func (m *mobileFileRepo) Reader(u fyne.URI) (fyne.URIReadCloser, error) { + return fileReaderForURI(u) +} + +func (m *mobileFileRepo) Writer(u fyne.URI) (fyne.URIWriteCloser, error) { + return fileWriterForURI(u, true) +} + +func (m *mobileFileRepo) Appender(u fyne.URI) (fyne.URIWriteCloser, error) { + return fileWriterForURI(u, false) +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/uri.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/uri.go new file mode 100644 index 0000000..8b2d05d --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/uri.go @@ -0,0 +1,16 @@ +//go:build !android + +package mobile + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/storage" +) + +func nativeURI(path string) fyne.URI { + uri, err := storage.ParseURI(path) + if err != nil { + fyne.LogError("Error on parsing uri", err) + } + return uri +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/uri_android.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/uri_android.go new file mode 100644 index 0000000..4c7ee88 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/uri_android.go @@ -0,0 +1,64 @@ +//go:build android + +package mobile + +/* +#cgo LDFLAGS: -landroid -llog + +#include + +char* contentURIGetFileName(uintptr_t jni_env, uintptr_t ctx, char* uriCstr); +*/ +import "C" + +import ( + "path/filepath" + "unsafe" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/driver/mobile/app" + "fyne.io/fyne/v2/storage" +) + +type androidURI struct { + systemURI string + fyne.URI +} + +// Override Name on android for content:// +func (a *androidURI) Name() string { + if a.Scheme() == "content" { + result := contentURIGetFileName(a.systemURI) + if result != "" { + return result + } + } + return a.URI.Name() +} + +func (a *androidURI) Extension() string { + return filepath.Ext(a.Name()) +} + +func contentURIGetFileName(uri string) string { + uriStr := C.CString(uri) + defer C.free(unsafe.Pointer(uriStr)) + + var filename string + app.RunOnJVM(func(_, env, ctx uintptr) error { + fnamePtr := C.contentURIGetFileName(C.uintptr_t(env), C.uintptr_t(ctx), uriStr) + vPtr := unsafe.Pointer(fnamePtr) + if vPtr == C.NULL { + return nil + } + filename = C.GoString(fnamePtr) + C.free(vPtr) + + return nil + }) + return filename +} + +func nativeURI(uri string) fyne.URI { + return &androidURI{URI: storage.NewURI(uri), systemURI: uri} +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/window.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/window.go new file mode 100644 index 0000000..85cdf09 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/window.go @@ -0,0 +1,209 @@ +package mobile + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/internal/cache" + "fyne.io/fyne/v2/internal/driver/common" + "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" +) + +type window struct { + title string + visible bool + onClosed func() + onCloseIntercepted func() + isChild bool + + canvas *canvas + icon fyne.Resource + menu *fyne.MainMenu + handle uintptr // the window handle - currently just Android +} + +func (w *window) Title() string { + return w.title +} + +func (w *window) SetTitle(title string) { + w.title = title +} + +func (w *window) FullScreen() bool { + return true +} + +func (w *window) SetFullScreen(bool) { + // no-op +} + +func (w *window) Resize(size fyne.Size) { + w.Canvas().(*canvas).Resize(size) +} + +func (w *window) RequestFocus() { + // no-op - we cannot change which window is focused +} + +func (w *window) FixedSize() bool { + return true +} + +func (w *window) SetFixedSize(bool) { + // no-op - all windows are fixed size +} + +func (w *window) CenterOnScreen() { + // no-op +} + +func (w *window) Padded() bool { + return w.canvas.padded +} + +func (w *window) SetPadded(padded bool) { + w.canvas.padded = padded +} + +func (w *window) Icon() fyne.Resource { + if w.icon == nil { + return fyne.CurrentApp().Icon() + } + + return w.icon +} + +func (w *window) SetIcon(icon fyne.Resource) { + w.icon = icon +} + +func (w *window) SetMaster() { + // no-op on mobile +} + +func (w *window) MainMenu() *fyne.MainMenu { + return w.menu +} + +func (w *window) SetMainMenu(menu *fyne.MainMenu) { + w.menu = menu +} + +func (w *window) SetOnClosed(callback func()) { + w.onClosed = callback +} + +func (w *window) SetCloseIntercept(callback func()) { + w.onCloseIntercepted = callback +} + +func (w *window) SetOnDropped(dropped func(fyne.Position, []fyne.URI)) { + // not implemented yet +} + +func (w *window) Show() { + menu := fyne.CurrentApp().Driver().(*driver).findMenu(w) + menuButton := w.newMenuButton(menu) + if menu == nil { + menuButton.Hide() + } + + if w.isChild { + exit := widget.NewButtonWithIcon("", theme.CancelIcon(), func() { + w.tryClose() + }) + title := widget.NewLabel(w.title) + title.Alignment = fyne.TextAlignCenter + w.canvas.setWindowHead(container.NewHBox(menuButton, + layout.NewSpacer(), title, layout.NewSpacer(), exit)) + w.canvas.Resize(w.canvas.size) + } else { + w.canvas.setWindowHead(container.NewHBox(menuButton)) + } + w.visible = true + + if w.Content() != nil { + w.Content().Refresh() + w.Content().Show() + } +} + +func (w *window) Hide() { + w.visible = false + + if w.Content() != nil { + w.Content().Hide() + } +} + +func (w *window) tryClose() { + if w.onCloseIntercepted != nil { + w.onCloseIntercepted() + return + } + + w.Close() +} + +func (w *window) Close() { + d := fyne.CurrentApp().Driver().(*driver) + pos := -1 + for i, win := range d.windows { + if win == w { + pos = i + } + } + if pos != -1 { + d.windows = append(d.windows[:pos], d.windows[pos+1:]...) + } + + cache.RangeTexturesFor(w.canvas, w.canvas.Painter().Free) + + w.canvas.WalkTrees(nil, func(node *common.RenderCacheNode, _ fyne.Position) { + if wid, ok := node.Obj().(fyne.Widget); ok { + cache.DestroyRenderer(wid) + } + }) + cache.CleanCanvas(w.canvas) + + if w.onClosed != nil { + w.onClosed() + } +} + +func (w *window) ShowAndRun() { + w.Show() + fyne.CurrentApp().Run() +} + +func (w *window) Content() fyne.CanvasObject { + return w.canvas.Content() +} + +func (w *window) SetContent(content fyne.CanvasObject) { + w.canvas.SetContent(content) +} + +func (w *window) Canvas() fyne.Canvas { + return w.canvas +} + +func (w *window) Clipboard() fyne.Clipboard { + return NewClipboard() +} + +func (w *window) RunWithContext(f func()) { + // ctx, _ = e.DrawContext.(gl.Context) + + f() +} + +func (w *window) RescaleContext() { + // TODO +} + +func (w *window) Context() any { + return fyne.CurrentApp().Driver().(*driver).glctx +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/window_android.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/window_android.go new file mode 100644 index 0000000..cb8b02d --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/window_android.go @@ -0,0 +1,26 @@ +//go:build android + +package mobile + +import ( + fyneDriver "fyne.io/fyne/v2/driver" + "fyne.io/fyne/v2/internal/driver/mobile/app" +) + +// Assert we are satisfying the driver.NativeWindow interface +var _ fyneDriver.NativeWindow = (*window)(nil) + +func (w *window) RunNative(f func(context any)) { + app.RunOnJVM(func(vm, env, ctx uintptr) error { + data := &fyneDriver.AndroidWindowContext{ + NativeWindow: w.handle, + AndroidContext: fyneDriver.AndroidContext{ + VM: vm, + Env: env, + Ctx: ctx, + }, + } + f(data) + return nil + }) +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/mobile/window_ios.go b/vendor/fyne.io/fyne/v2/internal/driver/mobile/window_ios.go new file mode 100644 index 0000000..cad67cb --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/mobile/window_ios.go @@ -0,0 +1,14 @@ +//go:build ios + +package mobile + +import ( + fyneDriver "fyne.io/fyne/v2/driver" +) + +// Assert we are satisfying the driver.NativeWindow interface +var _ fyneDriver.NativeWindow = (*window)(nil) + +func (w *window) RunNative(fn func(context any)) { + fn(&fyneDriver.UnknownContext{}) +} diff --git a/vendor/fyne.io/fyne/v2/internal/driver/util.go b/vendor/fyne.io/fyne/v2/internal/driver/util.go new file mode 100644 index 0000000..d5d62c3 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/driver/util.go @@ -0,0 +1,220 @@ +package driver + +import ( + "math" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/cache" +) + +// AbsolutePositionForObject returns the absolute position of an object in a set of object trees. +// If the object is not part of any of the trees, the position (0,0) is returned. +func AbsolutePositionForObject(object fyne.CanvasObject, trees []fyne.CanvasObject) fyne.Position { + var pos fyne.Position + findPos := func(o fyne.CanvasObject, p fyne.Position, _ fyne.Position, _ fyne.Size) bool { + if o == object { + pos = p + return true + } + return false + } + for _, tree := range trees { + if WalkVisibleObjectTree(tree, findPos, nil) { + break + } + } + return pos +} + +// FindObjectAtPositionMatching is used to find an object in a canvas at the specified position. +// The matches function determines of the type of object that is found at this position is of a suitable type. +// The various canvas roots and overlays that can be searched are also passed in. +func FindObjectAtPositionMatching(mouse fyne.Position, matches func(object fyne.CanvasObject) bool, overlay fyne.CanvasObject, roots ...fyne.CanvasObject) (fyne.CanvasObject, fyne.Position, int) { + var found fyne.CanvasObject + var foundPos fyne.Position + + findFunc := func(walked fyne.CanvasObject, pos fyne.Position, clipPos fyne.Position, clipSize fyne.Size) bool { + if !walked.Visible() { + return false + } + + if mouse.X < clipPos.X || mouse.Y < clipPos.Y { + return false + } + + if mouse.X >= clipPos.X+clipSize.Width || mouse.Y >= clipPos.Y+clipSize.Height { + return false + } + + if mouse.X < pos.X || mouse.Y < pos.Y { + return false + } + + if mouse.X >= pos.X+walked.Size().Width || mouse.Y >= pos.Y+walked.Size().Height { + return false + } + + if matches(walked) { + found = walked + foundPos = fyne.NewPos(mouse.X-pos.X, mouse.Y-pos.Y) + } + return false + } + + layer := 0 + if overlay != nil { + WalkVisibleObjectTree(overlay, findFunc, nil) + } else { + for _, root := range roots { + layer++ + if root == nil { + continue + } + WalkVisibleObjectTree(root, findFunc, nil) + if found != nil { + break + } + } + } + + return found, foundPos, layer +} + +// ReverseWalkVisibleObjectTree will walk an object tree in reverse order for all visible objects +// executing the passed functions following the following rules: +// - beforeChildren is called for the start obj before traversing its children +// - the obj's children are traversed by calling walkObjects on each of the visible items +// - afterChildren is called for the obj after traversing the obj's children +// The walk can be aborted by returning true in one of the functions: +// - if beforeChildren returns true, further traversing is stopped immediately, the after function +// will not be called for the obj where the walk stopped, however, it will be called for all its +// parents +func ReverseWalkVisibleObjectTree( + obj fyne.CanvasObject, + beforeChildren func(fyne.CanvasObject, fyne.Position, fyne.Position, fyne.Size) bool, + afterChildren func(fyne.CanvasObject, fyne.Position, fyne.CanvasObject), +) bool { + clipSize := fyne.NewSize(math.MaxInt32, math.MaxInt32) + return walkObjectTree(obj, true, nil, fyne.NewPos(0, 0), fyne.NewPos(0, 0), clipSize, beforeChildren, afterChildren, true) +} + +// WalkCompleteObjectTree will walk an object tree for all objects (ignoring visible state) executing the passed +// functions following the following rules: +// - beforeChildren is called for the start obj before traversing its children +// - the obj's children are traversed by calling walkObjects on each of the items +// - afterChildren is called for the obj after traversing the obj's children +// The walk can be aborted by returning true in one of the functions: +// - if beforeChildren returns true, further traversing is stopped immediately, the after function +// will not be called for the obj where the walk stopped, however, it will be called for all its +// parents +func WalkCompleteObjectTree( + obj fyne.CanvasObject, + beforeChildren func(fyne.CanvasObject, fyne.Position, fyne.Position, fyne.Size) bool, + afterChildren func(fyne.CanvasObject, fyne.Position, fyne.CanvasObject), +) bool { + clipSize := fyne.NewSize(math.MaxInt32, math.MaxInt32) + return walkObjectTree(obj, false, nil, fyne.NewPos(0, 0), fyne.NewPos(0, 0), clipSize, beforeChildren, afterChildren, false) +} + +// WalkVisibleObjectTree will walk an object tree for all visible objects executing the passed functions following +// the following rules: +// - beforeChildren is called for the start obj before traversing its children +// - the obj's children are traversed by calling walkObjects on each of the visible items +// - afterChildren is called for the obj after traversing the obj's children +// The walk can be aborted by returning true in one of the functions: +// - if beforeChildren returns true, further traversing is stopped immediately, the after function +// will not be called for the obj where the walk stopped, however, it will be called for all its +// parents +func WalkVisibleObjectTree( + obj fyne.CanvasObject, + beforeChildren func(fyne.CanvasObject, fyne.Position, fyne.Position, fyne.Size) bool, + afterChildren func(fyne.CanvasObject, fyne.Position, fyne.CanvasObject), +) bool { + clipSize := fyne.NewSize(math.MaxInt32, math.MaxInt32) + return walkObjectTree(obj, false, nil, fyne.NewPos(0, 0), fyne.NewPos(0, 0), clipSize, beforeChildren, afterChildren, true) +} + +func walkObjectTree( + obj fyne.CanvasObject, + reverse bool, + parent fyne.CanvasObject, + offset, clipPos fyne.Position, + clipSize fyne.Size, + beforeChildren func(fyne.CanvasObject, fyne.Position, fyne.Position, fyne.Size) bool, + afterChildren func(fyne.CanvasObject, fyne.Position, fyne.CanvasObject), + requireVisible bool, +) bool { + if obj == nil { + return false + } + if requireVisible && !obj.Visible() { + return false + } + pos := obj.Position().Add(offset) + + var children []fyne.CanvasObject + switch co := obj.(type) { + case *fyne.Container: + children = co.Objects + case fyne.Widget: + if cache.IsRendered(co) || requireVisible { + children = cache.Renderer(co).Objects() + } + } + + if IsClip(obj) { + clipPos = pos + clipSize = obj.Size() + } + + if beforeChildren != nil { + if beforeChildren(obj, pos, clipPos, clipSize) { + return true + } + } + + cancelled := false + followChild := func(child fyne.CanvasObject) bool { + if walkObjectTree(child, reverse, obj, pos, clipPos, clipSize, beforeChildren, afterChildren, requireVisible) { + cancelled = true + return true + } + return false + } + if reverse { + for i := len(children) - 1; i >= 0; i-- { + if followChild(children[i]) { + break + } + } + } else { + for _, child := range children { + if followChild(child) { + break + } + } + } + + if afterChildren != nil { + afterChildren(obj, pos, parent) + } + return cancelled +} + +func IsClip(o fyne.CanvasObject) bool { + _, scroll := o.(fyne.Scrollable) + if scroll { + return true + } + + if _, isWid := o.(fyne.Widget); !isWid { + return false + } + r, rendered := cache.CachedRenderer(o.(fyne.Widget)) + if !rendered { + return false + } + + _, clip := r.(interface{ IsClip() }) + return clip +} diff --git a/vendor/fyne.io/fyne/v2/internal/hints_disabled.go b/vendor/fyne.io/fyne/v2/internal/hints_disabled.go new file mode 100644 index 0000000..2809524 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/hints_disabled.go @@ -0,0 +1,9 @@ +//go:build !hints + +package internal + +// LogHint reports a developer hint that should be followed to improve their app. +// This does nothing unless the "hints" build flag is used. +func LogHint(reason string) { + // no-op when hints not enabled +} diff --git a/vendor/fyne.io/fyne/v2/internal/hints_enabled.go b/vendor/fyne.io/fyne/v2/internal/hints_enabled.go new file mode 100644 index 0000000..3e4935a --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/hints_enabled.go @@ -0,0 +1,18 @@ +//go:build hints + +package internal + +import ( + "log" + "runtime" +) + +// LogHint reports a developer hint that should be followed to improve their app. +func LogHint(reason string) { + log.Println("Fyne hint: ", reason) + + _, file, line, ok := runtime.Caller(2) + if ok { + log.Printf(" Created at: %s:%d", file, line) + } +} diff --git a/vendor/fyne.io/fyne/v2/internal/metadata/data.go b/vendor/fyne.io/fyne/v2/internal/metadata/data.go new file mode 100644 index 0000000..30288fe --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/metadata/data.go @@ -0,0 +1,34 @@ +package metadata + +// FyneApp describes the top level metadata for building a fyne application +type FyneApp struct { + Website string `toml:",omitempty"` + Details AppDetails + Development map[string]string `toml:",omitempty"` + Release map[string]string `toml:",omitempty"` + Source *AppSource `toml:",omitempty"` + LinuxAndBSD *LinuxAndBSD `toml:",omitempty"` + Languages []string `toml:",omitempty"` + Migrations map[string]bool `toml:",omitempty"` +} + +// AppDetails describes the build information, this group may be OS or arch specific +type AppDetails struct { + Icon string `toml:",omitempty"` + Name, ID string `toml:",omitempty"` + Version string `toml:",omitempty"` + Build int `toml:",omitempty"` +} + +type AppSource struct { + Repo, Dir string `toml:",omitempty"` +} + +// LinuxAndBSD describes specific metadata for desktop files on Linux and BSD. +type LinuxAndBSD struct { + GenericName string `toml:",omitempty"` + Categories []string `toml:",omitempty"` + Comment string `toml:",omitempty"` + Keywords []string `toml:",omitempty"` + ExecParams string `toml:",omitempty"` +} diff --git a/vendor/fyne.io/fyne/v2/internal/metadata/icon.go b/vendor/fyne.io/fyne/v2/internal/metadata/icon.go new file mode 100644 index 0000000..745c547 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/metadata/icon.go @@ -0,0 +1,41 @@ +package metadata + +import ( + "bytes" + "image/png" + "strconv" + + "github.com/nfnt/resize" + + "fyne.io/fyne/v2" +) + +func ScaleIcon(data fyne.Resource, size int) fyne.Resource { + img, err := png.Decode(bytes.NewReader(data.Content())) + if err != nil { + fyne.LogError("Failed to decode app icon", err) + return data + } + + if img.Bounds().Dx() <= size { + return data + } + + sized := resize.Resize(uint(size), uint(size), img, resize.Lanczos3) + smallData := &bytes.Buffer{} + err = png.Encode(smallData, sized) + if err != nil { + fyne.LogError("Failed to encode smaller app icon", err) + return data + } + + name := data.Name() + nameLen := len(name) + suffix := "-" + strconv.Itoa(size) + ".png" + if nameLen <= 4 || name[nameLen-4] != '.' { + name = "appicon" + suffix + } else { + name = name[:nameLen-4] + suffix + } + return fyne.NewStaticResource(name, smallData.Bytes()) +} diff --git a/vendor/fyne.io/fyne/v2/internal/metadata/load.go b/vendor/fyne.io/fyne/v2/internal/metadata/load.go new file mode 100644 index 0000000..29fb8a3 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/metadata/load.go @@ -0,0 +1,34 @@ +package metadata + +import ( + "io" + "os" + "path/filepath" + + "github.com/BurntSushi/toml" +) + +// Load attempts to read a FyneApp metadata from the provided reader. +// If this cannot be done an error will be returned. +func Load(r io.Reader) (*FyneApp, error) { + var data FyneApp + _, err := toml.NewDecoder(r).Decode(&data) + if err != nil { + return nil, err + } + + return &data, nil +} + +// LoadStandard attempts to read a FyneApp metadata from the `FyneApp.toml` file in the specified dir. +// If the file cannot be found or parsed an error will be returned. +func LoadStandard(dir string) (*FyneApp, error) { + path := filepath.Join(dir, "FyneApp.toml") + r, err := os.Open(path) + if err != nil { + return nil, err + } + + defer r.Close() + return Load(r) +} diff --git a/vendor/fyne.io/fyne/v2/internal/metadata/save.go b/vendor/fyne.io/fyne/v2/internal/metadata/save.go new file mode 100644 index 0000000..df49e1e --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/metadata/save.go @@ -0,0 +1,36 @@ +package metadata + +import ( + "bytes" + "io" + "os" + "path/filepath" + + "github.com/BurntSushi/toml" +) + +// Save attempts to write a FyneApp metadata to the provided writer. +// If the encoding fails an error will be returned. +func Save(f *FyneApp, w io.Writer) error { + var buf bytes.Buffer + err := toml.NewEncoder(&buf).Encode(f) + if err != nil { + return err + } + + _, err = w.Write(buf.Bytes()) + return err +} + +// SaveStandard attempts to save a FyneApp metadata to the `FyneApp.toml` file in the specified dir. +// If the file cannot be written or encoding fails an error will be returned. +func SaveStandard(f *FyneApp, dir string) error { + path := filepath.Join(dir, "FyneApp.toml") + w, err := os.Create(path) + if err != nil { + return err + } + + defer w.Close() + return Save(f, w) +} diff --git a/vendor/fyne.io/fyne/v2/internal/overlay_stack.go b/vendor/fyne.io/fyne/v2/internal/overlay_stack.go new file mode 100644 index 0000000..5c689a7 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/overlay_stack.go @@ -0,0 +1,91 @@ +package internal + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/app" + "fyne.io/fyne/v2/internal/widget" +) + +// OverlayStack allows stacking overlays on top of each other. +// Removing an overlay will also remove all overlays above it. +type OverlayStack struct { + OnChange func() + Canvas fyne.Canvas + focusManagers []*app.FocusManager + overlays []fyne.CanvasObject +} + +// Add puts an overlay on the stack. +func (s *OverlayStack) Add(overlay fyne.CanvasObject) { + if overlay == nil { + return + } + + if s.OnChange != nil { + defer s.OnChange() + } + + s.overlays = append(s.overlays, overlay) + + // TODO this should probably apply to all once #707 is addressed + if _, ok := overlay.(*widget.OverlayContainer); ok { + safePos, safeSize := s.Canvas.InteractiveArea() + + overlay.Resize(safeSize) + overlay.Move(safePos) + } + + s.focusManagers = append(s.focusManagers, app.NewFocusManager(overlay)) +} + +// List returns all overlays on the stack from bottom to top. +func (s *OverlayStack) List() []fyne.CanvasObject { + return s.overlays +} + +// ListFocusManagers returns all focus managers on the stack from bottom to top. +func (s *OverlayStack) ListFocusManagers() []*app.FocusManager { + return s.focusManagers +} + +// Remove deletes an overlay and all overlays above it from the stack. +func (s *OverlayStack) Remove(overlay fyne.CanvasObject) { + if s.OnChange != nil { + defer s.OnChange() + } + + overlayIdx := -1 + for i, o := range s.overlays { + if o == overlay { + overlayIdx = i + break + } + } + if overlayIdx == -1 { + return + } + // set removed elements in backing array to nil to release memory references + for i := overlayIdx; i < len(s.overlays); i++ { + s.overlays[i] = nil + s.focusManagers[i] = nil + } + s.overlays = s.overlays[:overlayIdx] + s.focusManagers = s.focusManagers[:overlayIdx] +} + +// Top returns the top-most overlay of the stack. +func (s *OverlayStack) Top() fyne.CanvasObject { + if len(s.overlays) == 0 { + return nil + } + return s.overlays[len(s.overlays)-1] +} + +// TopFocusManager returns the app.FocusManager assigned to the top-most overlay of the stack. +func (s *OverlayStack) TopFocusManager() *app.FocusManager { + if len(s.focusManagers) == 0 { + return nil + } + + return s.focusManagers[len(s.focusManagers)-1] +} diff --git a/vendor/fyne.io/fyne/v2/internal/painter/draw.go b/vendor/fyne.io/fyne/v2/internal/painter/draw.go new file mode 100644 index 0000000..b7f9271 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/painter/draw.go @@ -0,0 +1,673 @@ +package painter + +import ( + "image" + "image/color" + "math" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + + "github.com/srwiley/rasterx" + "golang.org/x/image/math/fixed" +) + +const quarterCircleControl = 1 - 0.55228 + +// DrawArc rasterizes the given arc object into an image. +// The scale function is used to understand how many pixels are required per unit of size. +// The arc is drawn from StartAngle to EndAngle (in degrees). +// 0°/360 is top, 90° is right, 180° is bottom, 270° is left +// 0°/-360 is top, -90° is left, -180° is bottom, -270° is right +func DrawArc(arc *canvas.Arc, vectorPad float32, scale func(float32) float32) *image.RGBA { + size := arc.Size() + + width := int(scale(size.Width + vectorPad*2)) + height := int(scale(size.Height + vectorPad*2)) + + raw := image.NewRGBA(image.Rect(0, 0, width, height)) + scanner := rasterx.NewScannerGV(int(size.Width), int(size.Height), raw, raw.Bounds()) + + centerX := float64(width) / 2 + centerY := float64(height) / 2 + + outerRadius := fyne.Min(size.Width, size.Height) / 2 + innerRadius := float32(float64(outerRadius) * math.Min(1.0, math.Max(0.0, float64(arc.CutoutRatio)))) + cornerRadius := fyne.Min(GetMaximumRadiusArc(outerRadius, innerRadius, arc.EndAngle-arc.StartAngle), arc.CornerRadius) + startAngle, endAngle := NormalizeArcAngles(arc.StartAngle, arc.EndAngle) + + // convert to radians + startRad := float64(startAngle * math.Pi / 180.0) + endRad := float64(endAngle * math.Pi / 180.0) + sweep := endRad - startRad + if sweep == 0 { + // nothing to draw + return raw + } + + if sweep > 2*math.Pi { + sweep = 2 * math.Pi + } else if sweep < -2*math.Pi { + sweep = -2 * math.Pi + } + + cornerRadius = scale(cornerRadius) + outerRadius = scale(outerRadius) + innerRadius = scale(innerRadius) + + if arc.FillColor != nil { + filler := rasterx.NewFiller(width, height, scanner) + filler.SetColor(arc.FillColor) + // rasterx.AddArc is not used because it does not support rounded corners + drawRoundArc(filler, centerX, centerY, float64(outerRadius), float64(innerRadius), startRad, sweep, float64(cornerRadius)) + filler.Draw() + } + + stroke := float64(scale(arc.StrokeWidth)) + if arc.StrokeColor != nil && stroke > 0 { + dasher := rasterx.NewDasher(width, height, scanner) + dasher.SetColor(arc.StrokeColor) + dasher.SetStroke(fixed.Int26_6(stroke*64), 0, nil, nil, nil, 0, nil, 0) + // rasterx.AddArc is not used because it does not support rounded corners + drawRoundArc(dasher, centerX, centerY, float64(outerRadius), float64(innerRadius), startRad, sweep, float64(cornerRadius)) + dasher.Draw() + } + + return raw +} + +// DrawCircle rasterizes the given circle object into an image. +// The bounds of the output image will be increased by vectorPad to allow for stroke overflow at the edges. +// The scale function is used to understand how many pixels are required per unit of size. +func DrawCircle(circle *canvas.Circle, vectorPad float32, scale func(float32) float32) *image.RGBA { + size := circle.Size() + radius := GetMaximumRadius(size) + + width := int(scale(size.Width + vectorPad*2)) + height := int(scale(size.Height + vectorPad*2)) + stroke := scale(circle.StrokeWidth) + + raw := image.NewRGBA(image.Rect(0, 0, width, height)) + scanner := rasterx.NewScannerGV(int(size.Width), int(size.Height), raw, raw.Bounds()) + + if circle.FillColor != nil { + filler := rasterx.NewFiller(width, height, scanner) + filler.SetColor(circle.FillColor) + rasterx.AddCircle(float64(width/2), float64(height/2), float64(scale(radius)), filler) + filler.Draw() + } + + dasher := rasterx.NewDasher(width, height, scanner) + dasher.SetColor(circle.StrokeColor) + dasher.SetStroke(fixed.Int26_6(float64(stroke)*64), 0, nil, nil, nil, 0, nil, 0) + rasterx.AddCircle(float64(width/2), float64(height/2), float64(scale(radius)), dasher) + dasher.Draw() + + return raw +} + +// DrawLine rasterizes the given line object into an image. +// The bounds of the output image will be increased by vectorPad to allow for stroke overflow at the edges. +// The scale function is used to understand how many pixels are required per unit of size. +func DrawLine(line *canvas.Line, vectorPad float32, scale func(float32) float32) *image.RGBA { + col := line.StrokeColor + size := line.Size() + width := int(scale(size.Width + vectorPad*2)) + height := int(scale(size.Height + vectorPad*2)) + stroke := scale(line.StrokeWidth) + if stroke < 1 { // software painter doesn't fade lines to compensate + stroke = 1 + } + + raw := image.NewRGBA(image.Rect(0, 0, width, height)) + scanner := rasterx.NewScannerGV(int(size.Width), int(size.Height), raw, raw.Bounds()) + dasher := rasterx.NewDasher(width, height, scanner) + dasher.SetColor(col) + dasher.SetStroke(fixed.Int26_6(float64(stroke)*64), 0, nil, nil, nil, 0, nil, 0) + position := line.Position() + p1x, p1y := scale(line.Position1.X-position.X+vectorPad), scale(line.Position1.Y-position.Y+vectorPad) + p2x, p2y := scale(line.Position2.X-position.X+vectorPad), scale(line.Position2.Y-position.Y+vectorPad) + + if stroke <= 1.5 { // adjust to support 1px + if p1x == p2x { + p1x -= 0.5 + p2x -= 0.5 + } + if p1y == p2y { + p1y -= 0.5 + p2y -= 0.5 + } + } + + dasher.Start(rasterx.ToFixedP(float64(p1x), float64(p1y))) + dasher.Line(rasterx.ToFixedP(float64(p2x), float64(p2y))) + dasher.Stop(true) + dasher.Draw() + + return raw +} + +// DrawPolygon rasterizes the given regular polygon object into an image. +// The bounds of the output image will be increased by vectorPad to allow for stroke overflow at the edges. +// The scale function is used to understand how many pixels are required per unit of size. +func DrawPolygon(polygon *canvas.Polygon, vectorPad float32, scale func(float32) float32) *image.RGBA { + size := polygon.Size() + + width := int(scale(size.Width + vectorPad*2)) + height := int(scale(size.Height + vectorPad*2)) + outerRadius := scale(fyne.Min(size.Width, size.Height) / 2) + cornerRadius := scale(fyne.Min(GetMaximumRadius(size), polygon.CornerRadius)) + sides := int(polygon.Sides) + angle := polygon.Angle + + raw := image.NewRGBA(image.Rect(0, 0, width, height)) + scanner := rasterx.NewScannerGV(int(size.Width), int(size.Height), raw, raw.Bounds()) + + if polygon.FillColor != nil { + filler := rasterx.NewFiller(width, height, scanner) + filler.SetColor(polygon.FillColor) + drawRegularPolygon(float64(width/2), float64(height/2), float64(outerRadius), float64(cornerRadius), float64(angle), int(sides), filler) + filler.Draw() + } + + if polygon.StrokeColor != nil && polygon.StrokeWidth > 0 { + dasher := rasterx.NewDasher(width, height, scanner) + dasher.SetColor(polygon.StrokeColor) + dasher.SetStroke(fixed.Int26_6(float64(scale(polygon.StrokeWidth))*64), 0, nil, nil, nil, 0, nil, 0) + drawRegularPolygon(float64(width/2), float64(height/2), float64(outerRadius), float64(cornerRadius), float64(angle), int(sides), dasher) + dasher.Draw() + } + + return raw +} + +// DrawRectangle rasterizes the given rectangle object with stroke border into an image. +// The bounds of the output image will be increased by vectorPad to allow for stroke overflow at the edges. +// The scale function is used to understand how many pixels are required per unit of size. +func DrawRectangle(rect *canvas.Rectangle, rWidth, rHeight, vectorPad float32, scale func(float32) float32) *image.RGBA { + topRightRadius := GetCornerRadius(rect.TopRightCornerRadius, rect.CornerRadius) + topLeftRadius := GetCornerRadius(rect.TopLeftCornerRadius, rect.CornerRadius) + bottomRightRadius := GetCornerRadius(rect.BottomRightCornerRadius, rect.CornerRadius) + bottomLeftRadius := GetCornerRadius(rect.BottomLeftCornerRadius, rect.CornerRadius) + return drawOblong(rect.FillColor, rect.StrokeColor, rect.StrokeWidth, topRightRadius, topLeftRadius, bottomRightRadius, bottomLeftRadius, rWidth, rHeight, vectorPad, scale) +} + +func drawOblong(fill, strokeCol color.Color, strokeWidth, topRightRadius, topLeftRadius, bottomRightRadius, bottomLeftRadius, rWidth, rHeight, vectorPad float32, scale func(float32) float32) *image.RGBA { + // The maximum possible corner radii for a circular shape + size := fyne.NewSize(rWidth, rHeight) + topRightRadius = GetMaximumCornerRadius(topRightRadius, topLeftRadius, bottomRightRadius, size) + topLeftRadius = GetMaximumCornerRadius(topLeftRadius, topRightRadius, bottomLeftRadius, size) + bottomRightRadius = GetMaximumCornerRadius(bottomRightRadius, bottomLeftRadius, topRightRadius, size) + bottomLeftRadius = GetMaximumCornerRadius(bottomLeftRadius, bottomRightRadius, topLeftRadius, size) + + width := int(scale(rWidth + vectorPad*2)) + height := int(scale(rHeight + vectorPad*2)) + stroke := scale(strokeWidth) + + raw := image.NewRGBA(image.Rect(0, 0, width, height)) + scanner := rasterx.NewScannerGV(int(rWidth), int(rHeight), raw, raw.Bounds()) + + scaledPad := scale(vectorPad) + p1x, p1y := scaledPad, scaledPad + p2x, p2y := scale(rWidth)+scaledPad, scaledPad + p3x, p3y := scale(rWidth)+scaledPad, scale(rHeight)+scaledPad + p4x, p4y := scaledPad, scale(rHeight)+scaledPad + + if fill != nil { + filler := rasterx.NewFiller(width, height, scanner) + filler.SetColor(fill) + if topRightRadius == topLeftRadius && bottomRightRadius == bottomLeftRadius && topRightRadius == bottomRightRadius { + // If all corners are the same, we can draw a simple rectangle + radius := topRightRadius + if radius == 0 { + rasterx.AddRect(float64(p1x), float64(p1y), float64(p3x), float64(p3y), 0, filler) + } else { + r := float64(scale(radius)) + rasterx.AddRoundRect(float64(p1x), float64(p1y), float64(p3x), float64(p3y), r, r, 0, rasterx.RoundGap, filler) + } + } else { + rTL, rTR, rBR, rBL := scale(topLeftRadius), scale(topRightRadius), scale(bottomRightRadius), scale(bottomLeftRadius) + // Top-left corner + c := quarterCircleControl * rTL + if c != 0 { + filler.Start(rasterx.ToFixedP(float64(p1x), float64(p1y+rTL))) + filler.CubeBezier(rasterx.ToFixedP(float64(p1x), float64(p1y+c)), rasterx.ToFixedP(float64(p1x+c), float64(p1y)), rasterx.ToFixedP(float64(p1x+rTL), float64(p1y))) + } else { + filler.Start(rasterx.ToFixedP(float64(p1x), float64(p1y))) + } + // Top edge to top-right + c = quarterCircleControl * rTR + filler.Line(rasterx.ToFixedP(float64(p2x-rTR), float64(p2y))) + if c != 0 { + filler.CubeBezier(rasterx.ToFixedP(float64(p2x-c), float64(p2y)), rasterx.ToFixedP(float64(p2x), float64(p2y+c)), rasterx.ToFixedP(float64(p2x), float64(p2y+rTR))) + } + // Right edge to bottom-right + c = quarterCircleControl * rBR + filler.Line(rasterx.ToFixedP(float64(p3x), float64(p3y-rBR))) + if c != 0 { + filler.CubeBezier(rasterx.ToFixedP(float64(p3x), float64(p3y-c)), rasterx.ToFixedP(float64(p3x-c), float64(p3y)), rasterx.ToFixedP(float64(p3x-rBR), float64(p3y))) + } + // Bottom edge to bottom-left + c = quarterCircleControl * rBL + filler.Line(rasterx.ToFixedP(float64(p4x+rBL), float64(p4y))) + if c != 0 { + filler.CubeBezier(rasterx.ToFixedP(float64(p4x+c), float64(p4y)), rasterx.ToFixedP(float64(p4x), float64(p4y-c)), rasterx.ToFixedP(float64(p4x), float64(p4y-rBL))) + } + // Left edge to top-left + filler.Line(rasterx.ToFixedP(float64(p1x), float64(p1y+rTL))) + filler.Stop(true) + } + filler.Draw() + } + + if strokeCol != nil && strokeWidth > 0 { + dasher := rasterx.NewDasher(width, height, scanner) + dasher.SetColor(strokeCol) + dasher.SetStroke(fixed.Int26_6(float64(stroke)*64), 0, nil, nil, nil, 0, nil, 0) + rTL, rTR, rBR, rBL := scale(topLeftRadius), scale(topRightRadius), scale(bottomRightRadius), scale(bottomLeftRadius) + c := quarterCircleControl * rTL + if c != 0 { + dasher.Start(rasterx.ToFixedP(float64(p1x), float64(p1y+rTL))) + dasher.CubeBezier(rasterx.ToFixedP(float64(p1x), float64(p1y+c)), rasterx.ToFixedP(float64(p1x+c), float64(p1y)), rasterx.ToFixedP(float64(p1x+rTL), float64(p2y))) + } else { + dasher.Start(rasterx.ToFixedP(float64(p1x), float64(p1y))) + } + c = quarterCircleControl * rTR + dasher.Line(rasterx.ToFixedP(float64(p2x-rTR), float64(p2y))) + if c != 0 { + dasher.CubeBezier(rasterx.ToFixedP(float64(p2x-c), float64(p2y)), rasterx.ToFixedP(float64(p2x), float64(p2y+c)), rasterx.ToFixedP(float64(p2x), float64(p2y+rTR))) + } + c = quarterCircleControl * rBR + dasher.Line(rasterx.ToFixedP(float64(p3x), float64(p3y-rBR))) + if c != 0 { + dasher.CubeBezier(rasterx.ToFixedP(float64(p3x), float64(p3y-c)), rasterx.ToFixedP(float64(p3x-c), float64(p3y)), rasterx.ToFixedP(float64(p3x-rBR), float64(p3y))) + } + c = quarterCircleControl * rBL + dasher.Line(rasterx.ToFixedP(float64(p4x+rBL), float64(p4y))) + if c != 0 { + dasher.CubeBezier(rasterx.ToFixedP(float64(p4x+c), float64(p4y)), rasterx.ToFixedP(float64(p4x), float64(p4y-c)), rasterx.ToFixedP(float64(p4x), float64(p4y-rBL))) + } + dasher.Stop(true) + dasher.Draw() + } + + return raw +} + +// drawRegularPolygon draws a regular n-sides centered at (cx,cy) with +// radius, rounded corners of cornerRadius, rotated by rot degrees. +func drawRegularPolygon(cx, cy, radius, cornerRadius, rot float64, sides int, p rasterx.Adder) { + if sides < 3 || radius <= 0 { + return + } + gf := rasterx.RoundGap + angleStep := 2 * math.Pi / float64(sides) + rotRads := rot*math.Pi/180 - math.Pi/2 + + // fully rounded, draw circle + if math.Min(cornerRadius, radius) == radius { + rasterx.AddCircle(cx, cy, radius, p) + return + } + + // sharp polygon fast path + if cornerRadius <= 0 { + x0 := cx + radius*math.Cos(rotRads) + y0 := cy + radius*math.Sin(rotRads) + p.Start(rasterx.ToFixedP(x0, y0)) + for i := 1; i < sides; i++ { + t := rotRads + angleStep*float64(i) + p.Line(rasterx.ToFixedP(cx+radius*math.Cos(t), cy+radius*math.Sin(t))) + } + p.Stop(true) + return + } + + norm := func(x, y float64) (nx, ny float64) { + l := math.Hypot(x, y) + if l == 0 { + return 0, 0 + } + return x / l, y / l + } + + // regular polygon vertices + xs := make([]float64, sides) + ys := make([]float64, sides) + for i := 0; i < sides; i++ { + t := rotRads + angleStep*float64(i) + xs[i] = cx + radius*math.Cos(t) + ys[i] = cy + radius*math.Sin(t) + } + + // interior angle and side length + alpha := math.Pi * (float64(sides) - 2) / float64(sides) + r := cornerRadius + + // distances for tangency and center placement + tTrim := r / math.Tan(alpha/2) // along each edge from vertex to tangency + d := r / math.Sin(alpha/2) // from vertex to arc center along interior bisector + + // precompute fillet geometry per vertex + type pt struct{ x, y float64 } + sPts := make([]pt, sides) // start tangency (on incoming edge) + vS := make([]pt, sides) // center->start vector + vE := make([]pt, sides) // center->end vector + cPts := make([]pt, sides) // arc centers + + for i := 0; i < sides; i++ { + prv := (i - 1 + sides) % sides + nxt := (i + 1) % sides + + // unit directions + uInX, uInY := xs[i]-xs[prv], ys[i]-ys[prv] // prev -> i + uOutX, uOutY := xs[nxt]-xs[i], ys[nxt]-ys[i] // i -> next + uInX, uInY = norm(uInX, uInY) + uOutX, uOutY = norm(uOutX, uOutY) + + // tangency points along edges from the vertex + sx, sy := xs[i]-uInX*tTrim, ys[i]-uInY*tTrim // incoming (toward prev) + ex, ey := xs[i]+uOutX*tTrim, ys[i]+uOutY*tTrim // outgoing (toward next) + + // interior bisector direction and arc center + bx, by := -uInX+uOutX, -uInY+uOutY + bx, by = norm(bx, by) + cxI, cyI := xs[i]+bx*d, ys[i]+by*d + + // center->tangent vectors + vsx, vsy := sx-cxI, sy-cyI + velx, vely := ex-cxI, ey-cyI + + sPts[i] = pt{sx, sy} + vS[i] = pt{vsx, vsy} + vE[i] = pt{velx, vely} + cPts[i] = pt{cxI, cyI} + } + + // start at s0, arc corner 0, then line+arc around, close last edge + p.Start(rasterx.ToFixedP(sPts[0].x, sPts[0].y)) + gf(p, + rasterx.ToFixedP(cPts[0].x, cPts[0].y), + rasterx.ToFixedP(vS[0].x, vS[0].y), + rasterx.ToFixedP(vE[0].x, vE[0].y), + ) + for i := 1; i < sides; i++ { + p.Line(rasterx.ToFixedP(sPts[i].x, sPts[i].y)) + gf(p, + rasterx.ToFixedP(cPts[i].x, cPts[i].y), + rasterx.ToFixedP(vS[i].x, vS[i].y), + rasterx.ToFixedP(vE[i].x, vE[i].y), + ) + } + p.Line(rasterx.ToFixedP(sPts[0].x, sPts[0].y)) + p.Stop(true) +} + +// drawRoundArc constructs a rounded pie slice or annular sector +// it uses the Unit circle coordinate system +func drawRoundArc(adder rasterx.Adder, cx, cy, outer, inner, start, sweep, cr float64) { + if sweep == 0 { + return + } + + cosSinPoint := func(cx, cy, r, ang float64) (x, y float64) { + return cx + r*math.Cos(ang), cy - r*math.Sin(ang) + } + + // addCircularArc appends a circular arc to the current path using cubic Bezier approximation. + // 'adder' must already be positioned at the arc start point. + // sweep is signed (positive = CCW, negative = CW). + addCircularArc := func(adder rasterx.Adder, cx, cy, r, start, sweep float64) { + if sweep == 0 || r == 0 { + return + } + segCount := int(math.Ceil(math.Abs(sweep) / (math.Pi / 2.0))) + da := sweep / float64(segCount) + + for i := 0; i < segCount; i++ { + a1 := start + float64(i)*da + a2 := a1 + da + + x1, y1 := cosSinPoint(cx, cy, r, a1) + x2, y2 := cosSinPoint(cx, cy, r, a2) + + k := 4.0 / 3.0 * math.Tan((a2-a1)/4.0) + // tangent unit vectors on our param (x = cx+rcos, y = cy-rsin) + c1x := x1 + k*r*(-math.Sin(a1)) + c1y := y1 + k*r*(-math.Cos(a1)) + c2x := x2 - k*r*(-math.Sin(a2)) + c2y := y2 - k*r*(-math.Cos(a2)) + + adder.CubeBezier( + rasterx.ToFixedP(c1x, c1y), + rasterx.ToFixedP(c2x, c2y), + rasterx.ToFixedP(x2, y2), + ) + } + } + + // full-circle/donut paths (two closed subpaths: outer CCW, inner CW if inner > 0) + if math.Abs(sweep) >= 2*math.Pi { + // outer loop (CCW) + ox, oy := cosSinPoint(cx, cy, outer, 0) + adder.Start(rasterx.ToFixedP(ox, oy)) + addCircularArc(adder, cx, cy, outer, 0, 2*math.Pi) + adder.Stop(true) + // inner loop reversed (CW) to create a hole + if inner > 0 { + ix, iy := cosSinPoint(cx, cy, inner, 0) + adder.Start(rasterx.ToFixedP(ix, iy)) + addCircularArc(adder, cx, cy, inner, 0, -2*math.Pi) + adder.Stop(true) + } + return + } + + if cr <= 0 { + // sharp-corner fallback + if inner <= 0 { + // pie slice + ox, oy := cosSinPoint(cx, cy, outer, start) + adder.Start(rasterx.ToFixedP(cx, cy)) + adder.Line(rasterx.ToFixedP(ox, oy)) + addCircularArc(adder, cx, cy, outer, start, sweep) + adder.Line(rasterx.ToFixedP(cx, cy)) + adder.Stop(true) + return + } + // annular sector + outerStartX, outerStartY := cosSinPoint(cx, cy, outer, start) + adder.Start(rasterx.ToFixedP(outerStartX, outerStartY)) + addCircularArc(adder, cx, cy, outer, start, sweep) + innerEndX, innerEndY := cosSinPoint(cx, cy, inner, start+sweep) + adder.Line(rasterx.ToFixedP(innerEndX, innerEndY)) + addCircularArc(adder, cx, cy, inner, start+sweep, -sweep) + adder.Stop(true) + return + } + + // rounded corners + sgn := 1.0 + if sweep < 0 { + sgn = -1.0 + } + absSweep := math.Abs(sweep) + + // clamp the corner radius if the value is too large + cr = math.Min(cr, outer/2) + + // trim angles due to rounds + sOut := math.Sqrt(math.Max(0, outer*(outer-2*cr))) + thetaOut := math.Atan2(cr, sOut) // positive + + crIn := math.Min(cr, 0.5*math.Min(outer-inner, math.Abs(sweep)*inner)) + var sIn, thetaIn float64 + if inner > 0 { + sIn = math.Sqrt(math.Max(0, inner*(inner+2*crIn))) + thetaIn = math.Atan2(crIn, sIn) + } + + // ensure the trim does not exceed half the sweep + thetaOut = math.Min(thetaOut, absSweep/2.0-1e-6) + if thetaOut < 0 { + thetaOut = 0 + } + if inner > 0 { + thetaIn = math.Min(thetaIn, absSweep/2.0-1e-6) + if thetaIn < 0 { + thetaIn = 0 + } + } + + // trimmed arc angles + startOuter := start + sgn*thetaOut + endOuter := start + sweep - sgn*thetaOut + + startInner := 0.0 + endInner := 0.0 + if inner > 0 { + startInner = start + sgn*thetaIn + endInner = start + sweep - sgn*thetaIn + } + + // direction frames at start/end radial lines + // start side + vSx, vSy := math.Cos(start), -math.Sin(start) + tSx, tSy := -math.Sin(start), -math.Cos(start) + nSx, nSy := sgn*tSx, sgn*tSy // interior side normal at start + + // end side + endRad := start + sweep + vEx, vEy := math.Cos(endRad), -math.Sin(endRad) + tEx, tEy := -math.Sin(endRad), -math.Cos(endRad) + nEx, nEy := -sgn*tEx, -sgn*tEy // interior side normal at end + + // key points on arcs + pOutStartX, pOutStartY := cosSinPoint(cx, cy, outer, startOuter) + pOutEndX, pOutEndY := cosSinPoint(cx, cy, outer, endOuter) + + var pInStartX, pInStartY, pInEndX, pInEndY float64 + if inner > 0 { + pInStartX, pInStartY = cosSinPoint(cx, cy, inner, startInner) + pInEndX, pInEndY = cosSinPoint(cx, cy, inner, endInner) + } + + angleAt := func(cx, cy, x, y float64) float64 { + return math.Atan2(cy-y, x-cx) + } + + // round geometry at start/end + // outer rounds + aOutSx, aOutSy := cx+sOut*vSx, cy+sOut*vSy // radial tangent (start) + fOutSx, fOutSy := aOutSx+cr*nSx, aOutSy+cr*nSy // round center (start) + aOutEx, aOutEy := cx+sOut*vEx, cy+sOut*vEy // radial tangent (end) + fOutEx, fOutEy := aOutEx+cr*nEx, aOutEy+cr*nEy // round center (end) + phiOutEndB := angleAt(fOutEx, fOutEy, pOutEndX, pOutEndY) // outer end trimmed point + phiOutEndA := angleAt(fOutEx, fOutEy, aOutEx, aOutEy) // end radial tangent + phiOutStartA := angleAt(fOutSx, fOutSy, aOutSx, aOutSy) // start radial tangent + phiOutStartB := angleAt(fOutSx, fOutSy, pOutStartX, pOutStartY) // outer start trimmed point + + // inner rounds + var aInSx, aInSy, fInSx, fInSy, aInEx, aInEy, fInEx, fInEy float64 + var phiInEndA, phiInEndB, phiInStartA, phiInStartB float64 + if inner > 0 { + aInSx, aInSy = cx+sIn*vSx, cy+sIn*vSy + fInSx, fInSy = aInSx+crIn*nSx, aInSy+crIn*nSy + aInEx, aInEy = cx+sIn*vEx, cy+sIn*vEy + fInEx, fInEy = aInEx+crIn*nEx, aInEy+crIn*nEy + + phiInEndA = angleAt(fInEx, fInEy, aInEx, aInEy) // end radial tangent + phiInEndB = angleAt(fInEx, fInEy, pInEndX, pInEndY) // inner end trimmed point + phiInStartB = angleAt(fInSx, fInSy, pInStartX, pInStartY) // inner start trimmed point + phiInStartA = angleAt(fInSx, fInSy, aInSx, aInSy) // start radial tangent + } + + angleDiff := func(delta float64) float64 { + return math.Atan2(math.Sin(delta), math.Cos(delta)) + } + + adder.Start(rasterx.ToFixedP(pOutStartX, pOutStartY)) // start at trimmed outer start + addCircularArc(adder, cx, cy, outer, startOuter, endOuter-startOuter) // outer arc (trimmed) + addCircularArc(adder, fOutEx, fOutEy, cr, phiOutEndB, angleDiff(phiOutEndA-phiOutEndB)) // end side: outer round to radial + + if inner > 0 { + adder.Line(rasterx.ToFixedP(aInEx, aInEy)) // end side: radial line to inner + addCircularArc(adder, fInEx, fInEy, crIn, phiInEndA, angleDiff(phiInEndB-phiInEndA)) // end side: inner round to inner arc + addCircularArc(adder, cx, cy, inner, endInner, startInner-endInner) // inner arc (reverse, trimmed) + addCircularArc(adder, fInSx, fInSy, crIn, phiInStartB, angleDiff(phiInStartA-phiInStartB)) // start side: inner round to radial + adder.Line(rasterx.ToFixedP(aOutSx, aOutSy)) // start side: radial line to outer + } else { + // pie slice: close via center with radial lines + adder.Line(rasterx.ToFixedP(cx, cy)) // to center from end side + adder.Line(rasterx.ToFixedP(aOutSx, aOutSy)) // to start-side radial tangent + } + + // start side: outer round from radial to outer start + addCircularArc(adder, fOutSx, fOutSy, cr, phiOutStartA, angleDiff(phiOutStartB-phiOutStartA)) + adder.Stop(true) +} + +// GetCornerRadius returns the effective corner radius for a rectangle or square corner. +// If the specific corner radius (perCornerRadius) is zero, it falls back to the baseCornerRadius. +// Otherwise, it uses the specific corner radius provided. +// +// This allows for per-corner customization while maintaining a default overall radius. +func GetCornerRadius(perCornerRadius, baseCornerRadius float32) float32 { + if perCornerRadius == 0.0 { + return baseCornerRadius + } + return perCornerRadius +} + +// GetMaximumRadius returns the maximum possible corner radius that fits within the given size. +// It calculates half of the smaller dimension (width or height) of the provided fyne.Size. +// +// This is typically used for drawing circular corners in rectangles, circles or squares with the same radius for all corners. +func GetMaximumRadius(size fyne.Size) float32 { + return fyne.Min(size.Height, size.Width) / 2 +} + +// GetMaximumCornerRadius returns the maximum possible corner radius for an individual corner, +// considering the specified corner radius, the radii of adjacent corners, and the maximum radii +// allowed for the width and height of the shape. Corner radius may utilize unused capacity from adjacent corners with radius smaller than maximum value +// so this corner can grow up to double the maximum radius of the smaller dimension (width or height) without causing overlaps. +// +// This is typically used for drawing circular corners in rectangles or squares with different corner radii. +func GetMaximumCornerRadius(radius, adjacentWidthRadius, adjacentHeightRadius float32, size fyne.Size) float32 { + maxWidthRadius := size.Width / 2 + maxHeightRadius := size.Height / 2 + // fast path: corner radius fits within both per-axis maxima + if radius <= fyne.Min(maxWidthRadius, maxHeightRadius) { + return radius + } + // expand per-axis limits by borrowing any unused capacity from adjacent corners + expandedMaxWidthRadius := 2*maxWidthRadius - fyne.Min(maxWidthRadius, adjacentWidthRadius) + expandedMaxHeightRadius := 2*maxHeightRadius - fyne.Min(maxHeightRadius, adjacentHeightRadius) + + // respect the smaller axis and never exceed the requested radius + expandedMaxRadius := fyne.Min(expandedMaxWidthRadius, expandedMaxHeightRadius) + return fyne.Min(expandedMaxRadius, radius) +} + +// GetMaximumRadiusArc returns the maximum possible corner radius for an arc segment based on the outer radius, +// inner radius, and sweep angle in degrees. +// It calculates half of the smaller dimension (thickness or effective length) of the provided arc parameters +func GetMaximumRadiusArc(outerRadius, innerRadius, sweepAngle float32) float32 { + // height (thickness), width (length) + thickness := outerRadius - innerRadius + // TODO: length formula can be improved to get a fully rounded (pill shape) outer edge for thin (small sweep) arc segments + span := math.Sin(0.5 * math.Min(math.Abs(float64(sweepAngle))*math.Pi/180.0, math.Pi)) // span in (0,1) + length := 1.5 * float64(outerRadius) * span / (1 + span) // no division-by-zero risk + + return GetMaximumRadius(fyne.NewSize( + thickness, float32(length), + )) +} + +// NormalizeArcAngles adjusts the given start and end angles for arc drawing. +// It converts the angles from the Unit circle coordinate system (where 0 degrees is along the positive X-axis) +// to the coordinate system used by the painter, where 0 degrees is at the top (12 o'clock position). +// The function also reverses the direction: positive is clockwise, negative is counter-clockwise +func NormalizeArcAngles(startAngle, endAngle float32) (float32, float32) { + return -(startAngle - 90), -(endAngle - 90) +} diff --git a/vendor/fyne.io/fyne/v2/internal/painter/font.go b/vendor/fyne.io/fyne/v2/internal/painter/font.go new file mode 100644 index 0000000..e32ba3c --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/painter/font.go @@ -0,0 +1,389 @@ +package painter + +import ( + "bytes" + "image/color" + "image/draw" + "math" + "strings" + "sync" + + "github.com/go-text/render" + "github.com/go-text/typesetting/di" + "github.com/go-text/typesetting/font" + "github.com/go-text/typesetting/fontscan" + "github.com/go-text/typesetting/language" + "github.com/go-text/typesetting/shaping" + "golang.org/x/image/math/fixed" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/async" + "fyne.io/fyne/v2/internal/cache" + "fyne.io/fyne/v2/lang" + "fyne.io/fyne/v2/theme" +) + +const ( + // DefaultTabWidth is the default width in spaces + DefaultTabWidth = 4 + + fontTabSpaceSize = 10 +) + +var ( + fm *fontscan.FontMap + fontScanLock sync.Mutex + loaded bool +) + +func loadMap() { + loaded = true + + fm = fontscan.NewFontMap(noopLogger{}) + err := loadSystemFonts(fm) + if err != nil { + fm = nil // just don't fallback + } +} + +func lookupLangFont(family string, aspect font.Aspect) *font.Face { + fontScanLock.Lock() + defer fontScanLock.Unlock() + + if !loaded { + loadMap() + } + if fm == nil { + return nil + } + + fm.SetQuery(fontscan.Query{Families: []string{family}, Aspect: aspect}) + l, _ := fontscan.NewLangID(language.Language(lang.SystemLocale().LanguageString())) + return fm.ResolveFaceForLang(l) +} + +func lookupRuneFont(r rune, family string, aspect font.Aspect) *font.Face { + fontScanLock.Lock() + defer fontScanLock.Unlock() + + if !loaded { + loadMap() + } + if fm == nil { + return nil + } + + fm.SetQuery(fontscan.Query{Families: []string{family}, Aspect: aspect}) + return fm.ResolveFace(r) +} + +func lookupFaces(theme, fallback, emoji fyne.Resource, family string, style fyne.TextStyle) (faces *dynamicFontMap) { + f1 := loadMeasureFont(theme) + if theme == fallback { + faces = &dynamicFontMap{family: family, faces: []*font.Face{f1}} + } else { + f2 := loadMeasureFont(fallback) + faces = &dynamicFontMap{family: family, faces: []*font.Face{f1, f2}} + } + + aspect := font.Aspect{Style: font.StyleNormal} + if style.Italic { + aspect.Style = font.StyleItalic + } + if style.Bold { + aspect.Weight = font.WeightBold + } + + if emoji != nil { + faces.addFace(loadMeasureFont(emoji)) + } + + local := lookupLangFont(family, aspect) + if local != nil { + faces.addFace(local) + } + + return faces +} + +// CachedFontFace returns a Font face held in memory. These are loaded from the current theme. +func CachedFontFace(style fyne.TextStyle, source fyne.Resource, o fyne.CanvasObject) *FontCacheItem { + if source != nil { + val, ok := fontCustomCache.Load(source) + if !ok { + face := loadMeasureFont(source) + if face == nil { + face = loadMeasureFont(theme.TextFont()) + } + faces := &dynamicFontMap{family: source.Name(), faces: []*font.Face{face}} + + val = &FontCacheItem{Fonts: faces} + fontCustomCache.Store(source, val) + } + return val + } + + scope := "" + if o != nil { // for overridden themes get the cache key right + scope = cache.WidgetScopeID(o) + } + + val, ok := fontCache.Load(cacheID{style: style, scope: scope}) + if !ok { + var faces *dynamicFontMap + + th := theme.CurrentForWidget(o) + font1 := th.Font(style) + + emoji := theme.DefaultEmojiFont() // TODO only one emoji - maybe others too + switch { + case style.Monospace: + faces = lookupFaces(font1, theme.DefaultTextMonospaceFont(), emoji, fontscan.Monospace, style) + case style.Bold: + if style.Italic { + faces = lookupFaces(font1, theme.DefaultTextBoldItalicFont(), emoji, fontscan.SansSerif, style) + } else { + faces = lookupFaces(font1, theme.DefaultTextBoldFont(), emoji, fontscan.SansSerif, style) + } + case style.Italic: + faces = lookupFaces(font1, theme.DefaultTextItalicFont(), emoji, fontscan.SansSerif, style) + case style.Symbol: + th := theme.SymbolFont() + fallback := theme.DefaultSymbolFont() + f1 := loadMeasureFont(th) + + if th == fallback { + faces = &dynamicFontMap{family: fontscan.SansSerif, faces: []*font.Face{f1}} + } else { + f2 := loadMeasureFont(fallback) + faces = &dynamicFontMap{family: fontscan.SansSerif, faces: []*font.Face{f1, f2}} + } + default: + faces = lookupFaces(font1, theme.DefaultTextFont(), emoji, fontscan.SansSerif, style) + } + + val = &FontCacheItem{Fonts: faces} + fontCache.Store(cacheID{style: style, scope: scope}, val) + } + + return val +} + +// ClearFontCache is used to remove cached fonts in the case that we wish to re-load Font faces +func ClearFontCache() { + fontCache.Clear() + fontCustomCache.Clear() +} + +// DrawString draws a string into an image. +func DrawString(dst draw.Image, s string, color color.Color, f shaping.Fontmap, fontSize, scale float32, style fyne.TextStyle) { + r := render.Renderer{ + FontSize: fontSize, + PixScale: scale, + Color: color, + } + + advance := float32(0) + y := math.MinInt + walkString(f, s, float32ToFixed266(fontSize), style, &advance, scale, func(run shaping.Output, x float32) { + if y == math.MinInt { + y = int(math.Ceil(float64(fixed266ToFloat32(run.LineBounds.Ascent) * r.PixScale))) + } + if len(run.Glyphs) == 1 { + if run.Glyphs[0].GlyphID == 0 { + r.DrawStringAt(string([]rune{0xfffd}), dst, int(x), y, f.ResolveFace(0xfffd)) + return + } + } + + r.DrawShapedRunAt(run, dst, int(x), y) + }) +} + +func loadMeasureFont(data fyne.Resource) *font.Face { + loaded, err := font.ParseTTF(bytes.NewReader(data.Content())) + if err != nil { + fyne.LogError("font load error", err) + return nil + } + + return loaded +} + +// MeasureString returns how far dot would advance by drawing s with f. +// Tabs are translated into a dot location change. +func MeasureString(f shaping.Fontmap, s string, textSize float32, style fyne.TextStyle) (size fyne.Size, advance float32) { + return walkString(f, s, float32ToFixed266(textSize), style, &advance, 1, func(shaping.Output, float32) {}) +} + +// RenderedTextSize looks up how big a string would be if drawn on screen. +// It also returns the distance from top to the text baseline. +func RenderedTextSize(text string, fontSize float32, style fyne.TextStyle, source fyne.Resource) (size fyne.Size, baseline float32) { + size, base := cache.GetFontMetrics(text, fontSize, style, source) + if base != 0 { + return size, base + } + + size, base = measureText(text, fontSize, style, source) + cache.SetFontMetrics(text, fontSize, style, source, size, base) + return size, base +} + +func fixed266ToFloat32(i fixed.Int26_6) float32 { + return float32(float64(i) / (1 << 6)) +} + +func float32ToFixed266(f float32) fixed.Int26_6 { + return fixed.Int26_6(float64(f) * (1 << 6)) +} + +func measureText(text string, fontSize float32, style fyne.TextStyle, source fyne.Resource) (fyne.Size, float32) { + face := CachedFontFace(style, source, nil) + return MeasureString(face.Fonts, text, fontSize, style) +} + +func tabStop(spacew, x float32, tabWidth int) float32 { + if tabWidth <= 0 { + tabWidth = DefaultTabWidth + } + + tabw := spacew * float32(tabWidth) + tabs, _ := math.Modf(float64((x + tabw) / tabw)) + return tabw * float32(tabs) +} + +func walkString(faces shaping.Fontmap, s string, textSize fixed.Int26_6, style fyne.TextStyle, advance *float32, scale float32, + cb func(run shaping.Output, x float32), +) (size fyne.Size, base float32) { + s = strings.ReplaceAll(s, "\r", "") + + runes := []rune(s) + in := shaping.Input{ + Text: []rune{' '}, + RunStart: 0, + RunEnd: 1, + Direction: di.DirectionLTR, + Face: faces.ResolveFace(' '), + Size: textSize, + } + shaper := &shaping.HarfbuzzShaper{} + segmenter := &shaping.Segmenter{} + out := shaper.Shape(in) + + in.Text = runes + in.RunStart = 0 + in.RunEnd = len(runes) + + x := float32(0) + spacew := scale * fontTabSpaceSize + if style.Monospace { + spacew = scale * fixed266ToFloat32(out.Advance) + } + ins := segmenter.Split(in, faces) + for _, in := range ins { + inEnd := in.RunEnd + + pending := false + for i, r := range in.Text[in.RunStart:in.RunEnd] { + if r == '\t' { + if pending { + in.RunEnd = i + x = shapeCallback(shaper, in, x, scale, cb) + } + x = tabStop(spacew, x, style.TabWidth) + + in.RunStart = i + 1 + in.RunEnd = inEnd + pending = false + } else { + pending = true + } + } + + x = shapeCallback(shaper, in, x, scale, cb) + } + + *advance = x + return fyne.NewSize(*advance, fixed266ToFloat32(out.LineBounds.LineThickness())), + fixed266ToFloat32(out.LineBounds.Ascent) +} + +func shapeCallback(shaper shaping.Shaper, in shaping.Input, x, scale float32, cb func(shaping.Output, float32)) float32 { + out := shaper.Shape(in) + glyphs := out.Glyphs + start := 0 + pending := false + adv := fixed.I(0) + for i, g := range out.Glyphs { + if g.GlyphID == 0 { + if pending { + out.Glyphs = glyphs[start:i] + cb(out, x) + x += fixed266ToFloat32(adv) * scale + adv = 0 + } + + out.Glyphs = glyphs[i : i+1] + cb(out, x) + x += fixed266ToFloat32(glyphs[i].XAdvance) * scale + adv = 0 + + start = i + 1 + pending = false + } else { + pending = true + } + adv += g.XAdvance + } + + if pending { + out.Glyphs = glyphs[start:] + cb(out, x) + x += fixed266ToFloat32(adv) * scale + adv = 0 + } + return x + fixed266ToFloat32(adv)*scale +} + +type FontCacheItem struct { + Fonts shaping.Fontmap +} + +type cacheID struct { + style fyne.TextStyle + scope string +} + +var ( + fontCache async.Map[cacheID, *FontCacheItem] + fontCustomCache async.Map[fyne.Resource, *FontCacheItem] // for custom resources +) + +type noopLogger struct{} + +func (n noopLogger) Printf(string, ...any) {} + +type dynamicFontMap struct { + faces []*font.Face + family string +} + +func (d *dynamicFontMap) ResolveFace(r rune) *font.Face { + for _, f := range d.faces { + if _, ok := f.NominalGlyph(r); ok { + return f + } + } + + toAdd := lookupRuneFont(r, d.family, font.Aspect{}) + if toAdd != nil { + d.addFace(toAdd) + return toAdd + } + + return d.faces[0] +} + +func (d *dynamicFontMap) addFace(f *font.Face) { + d.faces = append(d.faces, f) +} diff --git a/vendor/fyne.io/fyne/v2/internal/painter/font_prod.go b/vendor/fyne.io/fyne/v2/internal/painter/font_prod.go new file mode 100644 index 0000000..fb57e31 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/painter/font_prod.go @@ -0,0 +1,21 @@ +//go:build !test + +package painter + +import ( + "os" + "path/filepath" + "runtime" + + "github.com/go-text/typesetting/fontscan" +) + +func loadSystemFonts(fm *fontscan.FontMap) error { + cacheDir := "" + if runtime.GOOS == "android" { + parent := os.Getenv("FILESDIR") + cacheDir = filepath.Join(parent, "fontcache") + } + + return fm.UseSystemFonts(cacheDir) +} diff --git a/vendor/fyne.io/fyne/v2/internal/painter/gl/capture.go b/vendor/fyne.io/fyne/v2/internal/painter/gl/capture.go new file mode 100644 index 0000000..ec3a922 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/painter/gl/capture.go @@ -0,0 +1,45 @@ +package gl + +import ( + "image" + "image/color" + + "fyne.io/fyne/v2" +) + +type captureImage struct { + pix []uint8 + width, height int +} + +func (c *captureImage) ColorModel() color.Model { + return color.RGBAModel +} + +func (c *captureImage) Bounds() image.Rectangle { + return image.Rect(0, 0, c.width, c.height) +} + +func (c *captureImage) At(x, y int) color.Color { + start := ((c.height-y-1)*c.width + x) * 4 + return color.RGBA{R: c.pix[start], G: c.pix[start+1], B: c.pix[start+2], A: c.pix[start+3]} +} + +func (p *painter) Capture(c fyne.Canvas) image.Image { + pos := fyne.NewPos(c.Size().Width, c.Size().Height) + width, height := c.PixelCoordinateForPosition(pos) + pixels := make([]uint8, width*height*4) + + p.contextProvider.RunWithContext(func() { + p.ctx.ReadBuffer(front) + p.logError() + p.ctx.ReadPixels(0, 0, width, height, colorFormatRGBA, unsignedByte, pixels) + p.logError() + }) + + return &captureImage{ + pix: pixels, + width: width, + height: height, + } +} diff --git a/vendor/fyne.io/fyne/v2/internal/painter/gl/context.go b/vendor/fyne.io/fyne/v2/internal/painter/gl/context.go new file mode 100644 index 0000000..942c490 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/painter/gl/context.go @@ -0,0 +1,45 @@ +package gl + +type context interface { + ActiveTexture(textureUnit uint32) + AttachShader(program Program, shader Shader) + BindBuffer(target uint32, buf Buffer) + BindTexture(target uint32, texture Texture) + BlendColor(r, g, b, a float32) + BlendFunc(srcFactor, destFactor uint32) + BufferData(target uint32, points []float32, usage uint32) + BufferSubData(target uint32, points []float32) + Clear(mask uint32) + ClearColor(r, g, b, a float32) + CompileShader(shader Shader) + CreateBuffer() Buffer + CreateProgram() Program + CreateShader(typ uint32) Shader + CreateTexture() Texture + DeleteBuffer(buffer Buffer) + DeleteTexture(texture Texture) + Disable(capability uint32) + DrawArrays(mode uint32, first, count int) + Enable(capability uint32) + EnableVertexAttribArray(attribute Attribute) + GetAttribLocation(program Program, name string) Attribute + GetError() uint32 + GetProgrami(program Program, param uint32) int + GetProgramInfoLog(program Program) string + GetShaderi(shader Shader, param uint32) int + GetShaderInfoLog(shader Shader) string + GetUniformLocation(program Program, name string) Uniform + LinkProgram(program Program) + ReadBuffer(src uint32) + ReadPixels(x, y, width, height int, colorFormat, typ uint32, pixels []uint8) + Scissor(x, y, w, h int32) + ShaderSource(shader Shader, source string) + TexImage2D(target uint32, level, width, height int, colorFormat, typ uint32, data []uint8) + TexParameteri(target, param uint32, value int32) + Uniform1f(uniform Uniform, v float32) + Uniform2f(uniform Uniform, v0, v1 float32) + Uniform4f(uniform Uniform, v0, v1, v2, v3 float32) + UseProgram(program Program) + VertexAttribPointerWithOffset(attribute Attribute, size int, typ uint32, normalized bool, stride, offset int) + Viewport(x, y, width, height int) +} diff --git a/vendor/fyne.io/fyne/v2/internal/painter/gl/draw.go b/vendor/fyne.io/fyne/v2/internal/painter/gl/draw.go new file mode 100644 index 0000000..53fd213 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/painter/gl/draw.go @@ -0,0 +1,642 @@ +package gl + +import ( + "image/color" + "math" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + paint "fyne.io/fyne/v2/internal/painter" +) + +const edgeSoftness = 1.0 + +func (p *painter) createBuffer(size int) Buffer { + vbo := p.ctx.CreateBuffer() + p.logError() + p.ctx.BindBuffer(arrayBuffer, vbo) + p.logError() + p.ctx.BufferData(arrayBuffer, make([]float32, size), staticDraw) + p.logError() + return vbo +} + +func (p *painter) drawCircle(circle *canvas.Circle, pos fyne.Position, frame fyne.Size) { + radius := paint.GetMaximumRadius(circle.Size()) + program := p.roundRectangleProgram + + // Vertex: BEG + bounds, points := p.vecSquareCoords(pos, circle, frame) + p.ctx.UseProgram(program.ref) + p.updateBuffer(program.buff, points) + p.UpdateVertexArray(program, "vert", 2, 4, 0) + p.UpdateVertexArray(program, "normal", 2, 4, 2) + + p.ctx.BlendFunc(srcAlpha, oneMinusSrcAlpha) + p.logError() + // Vertex: END + + // Fragment: BEG + frameWidthScaled, frameHeightScaled := p.scaleFrameSize(frame) + p.SetUniform2f(program, "frame_size", frameWidthScaled, frameHeightScaled) + + x1Scaled, x2Scaled, y1Scaled, y2Scaled := p.scaleRectCoords(bounds[0], bounds[2], bounds[1], bounds[3]) + p.SetUniform4f(program, "rect_coords", x1Scaled, x2Scaled, y1Scaled, y2Scaled) + + strokeWidthScaled := roundToPixel(circle.StrokeWidth*p.pixScale, 1.0) + p.SetUniform1f(program, "stroke_width_half", strokeWidthScaled*0.5) + + rectSizeWidthScaled := x2Scaled - x1Scaled - strokeWidthScaled + rectSizeHeightScaled := y2Scaled - y1Scaled - strokeWidthScaled + p.SetUniform2f(program, "rect_size_half", rectSizeWidthScaled*0.5, rectSizeHeightScaled*0.5) + + radiusScaled := roundToPixel(radius*p.pixScale, 1.0) + p.SetUniform4f(program, "radius", radiusScaled, radiusScaled, radiusScaled, radiusScaled) + + r, g, b, a := getFragmentColor(circle.FillColor) + p.SetUniform4f(program, "fill_color", r, g, b, a) + + strokeColor := circle.StrokeColor + if strokeColor == nil { + strokeColor = color.Transparent + } + r, g, b, a = getFragmentColor(strokeColor) + p.SetUniform4f(program, "stroke_color", r, g, b, a) + + edgeSoftnessScaled := roundToPixel(edgeSoftness*p.pixScale, 1.0) + p.SetUniform1f(program, "edge_softness", edgeSoftnessScaled) + p.logError() + // Fragment: END + + p.ctx.DrawArrays(triangleStrip, 0, 4) + p.logError() +} + +func (p *painter) drawGradient(o fyne.CanvasObject, texCreator func(fyne.CanvasObject) Texture, pos fyne.Position, frame fyne.Size) { + p.drawTextureWithDetails(o, texCreator, pos, o.Size(), frame, canvas.ImageFillStretch, 1.0, 0) +} + +func (p *painter) drawImage(img *canvas.Image, pos fyne.Position, frame fyne.Size) { + p.drawTextureWithDetails(img, p.newGlImageTexture, pos, img.Size(), frame, img.FillMode, float32(img.Alpha()), 0) +} + +func (p *painter) drawLine(line *canvas.Line, pos fyne.Position, frame fyne.Size) { + if line.StrokeColor == color.Transparent || line.StrokeColor == nil || line.StrokeWidth == 0 { + return + } + points, halfWidth, feather := p.lineCoords(pos, line.Position1, line.Position2, line.StrokeWidth, 0.5, frame) + p.ctx.UseProgram(p.lineProgram.ref) + p.updateBuffer(p.lineProgram.buff, points) + p.UpdateVertexArray(p.lineProgram, "vert", 2, 4, 0) + p.UpdateVertexArray(p.lineProgram, "normal", 2, 4, 2) + + p.ctx.BlendFunc(srcAlpha, oneMinusSrcAlpha) + p.logError() + + r, g, b, a := getFragmentColor(line.StrokeColor) + p.SetUniform4f(p.lineProgram, "color", r, g, b, a) + + p.SetUniform1f(p.lineProgram, "lineWidth", halfWidth) + + p.SetUniform1f(p.lineProgram, "feather", feather) + + p.ctx.DrawArrays(triangles, 0, 6) + p.logError() +} + +func (p *painter) drawObject(o fyne.CanvasObject, pos fyne.Position, frame fyne.Size) { + switch obj := o.(type) { + case *canvas.Circle: + p.drawCircle(obj, pos, frame) + case *canvas.Line: + p.drawLine(obj, pos, frame) + case *canvas.Image: + p.drawImage(obj, pos, frame) + case *canvas.Raster: + p.drawRaster(obj, pos, frame) + case *canvas.Rectangle: + p.drawRectangle(obj, pos, frame) + case *canvas.Text: + p.drawText(obj, pos, frame) + case *canvas.LinearGradient: + p.drawGradient(obj, p.newGlLinearGradientTexture, pos, frame) + case *canvas.RadialGradient: + p.drawGradient(obj, p.newGlRadialGradientTexture, pos, frame) + case *canvas.Polygon: + p.drawPolygon(obj, pos, frame) + case *canvas.Arc: + p.drawArc(obj, pos, frame) + } +} + +func (p *painter) drawRaster(img *canvas.Raster, pos fyne.Position, frame fyne.Size) { + p.drawTextureWithDetails(img, p.newGlRasterTexture, pos, img.Size(), frame, canvas.ImageFillStretch, float32(img.Alpha()), 0) +} + +func (p *painter) drawRectangle(rect *canvas.Rectangle, pos fyne.Position, frame fyne.Size) { + topRightRadius := paint.GetCornerRadius(rect.TopRightCornerRadius, rect.CornerRadius) + topLeftRadius := paint.GetCornerRadius(rect.TopLeftCornerRadius, rect.CornerRadius) + bottomRightRadius := paint.GetCornerRadius(rect.BottomRightCornerRadius, rect.CornerRadius) + bottomLeftRadius := paint.GetCornerRadius(rect.BottomLeftCornerRadius, rect.CornerRadius) + p.drawOblong(rect, rect.FillColor, rect.StrokeColor, rect.StrokeWidth, topRightRadius, topLeftRadius, bottomRightRadius, bottomLeftRadius, rect.Aspect, pos, frame) +} + +func (p *painter) drawOblong(obj fyne.CanvasObject, fill, stroke color.Color, strokeWidth, topRightRadius, topLeftRadius, bottomRightRadius, bottomLeftRadius, aspect float32, pos fyne.Position, frame fyne.Size) { + if (fill == color.Transparent || fill == nil) && (stroke == color.Transparent || stroke == nil || strokeWidth == 0) { + return + } + + roundedCorners := topRightRadius != 0 || topLeftRadius != 0 || bottomRightRadius != 0 || bottomLeftRadius != 0 + var program ProgramState + if roundedCorners { + program = p.roundRectangleProgram + } else { + program = p.rectangleProgram + } + + // Vertex: BEG + bounds, points := p.vecRectCoords(pos, obj, frame, aspect) + p.ctx.UseProgram(program.ref) + p.updateBuffer(program.buff, points) + p.UpdateVertexArray(program, "vert", 2, 4, 0) + p.UpdateVertexArray(program, "normal", 2, 4, 2) + + p.ctx.BlendFunc(srcAlpha, oneMinusSrcAlpha) + p.logError() + // Vertex: END + + // Fragment: BEG + frameWidthScaled, frameHeightScaled := p.scaleFrameSize(frame) + p.SetUniform2f(program, "frame_size", frameWidthScaled, frameHeightScaled) + + x1Scaled, x2Scaled, y1Scaled, y2Scaled := p.scaleRectCoords(bounds[0], bounds[2], bounds[1], bounds[3]) + p.SetUniform4f(program, "rect_coords", x1Scaled, x2Scaled, y1Scaled, y2Scaled) + + strokeWidthScaled := roundToPixel(strokeWidth*p.pixScale, 1.0) + if roundedCorners { + p.SetUniform1f(program, "stroke_width_half", strokeWidthScaled*0.5) + + rectSizeWidthScaled := x2Scaled - x1Scaled - strokeWidthScaled + rectSizeHeightScaled := y2Scaled - y1Scaled - strokeWidthScaled + p.SetUniform2f(program, "rect_size_half", rectSizeWidthScaled*0.5, rectSizeHeightScaled*0.5) + + // the maximum possible corner radii for a circular shape, calculated taking into account the rect coords with aspect ratio + size := fyne.NewSize(bounds[2]-bounds[0], bounds[3]-bounds[1]) + topRightRadiusScaled := roundToPixel( + paint.GetMaximumCornerRadius(topRightRadius, topLeftRadius, bottomRightRadius, size)*p.pixScale, + 1.0, + ) + topLeftRadiusScaled := roundToPixel( + paint.GetMaximumCornerRadius(topLeftRadius, topRightRadius, bottomLeftRadius, size)*p.pixScale, + 1.0, + ) + bottomRightRadiusScaled := roundToPixel( + paint.GetMaximumCornerRadius(bottomRightRadius, bottomLeftRadius, topRightRadius, size)*p.pixScale, + 1.0, + ) + bottomLeftRadiusScaled := roundToPixel( + paint.GetMaximumCornerRadius(bottomLeftRadius, bottomRightRadius, topLeftRadius, size)*p.pixScale, + 1.0, + ) + p.SetUniform4f(program, "radius", topRightRadiusScaled, bottomRightRadiusScaled, topLeftRadiusScaled, bottomLeftRadiusScaled) + + edgeSoftnessScaled := roundToPixel(edgeSoftness*p.pixScale, 1.0) + p.SetUniform1f(program, "edge_softness", edgeSoftnessScaled) + } else { + p.SetUniform1f(program, "stroke_width", strokeWidthScaled) + } + + r, g, b, a := getFragmentColor(fill) + p.SetUniform4f(program, "fill_color", r, g, b, a) + + strokeColor := stroke + if strokeColor == nil { + strokeColor = color.Transparent + } + r, g, b, a = getFragmentColor(strokeColor) + p.SetUniform4f(program, "stroke_color", r, g, b, a) + p.logError() + // Fragment: END + + p.ctx.DrawArrays(triangleStrip, 0, 4) + p.logError() +} + +func (p *painter) drawPolygon(polygon *canvas.Polygon, pos fyne.Position, frame fyne.Size) { + if ((polygon.FillColor == color.Transparent || polygon.FillColor == nil) && (polygon.StrokeColor == color.Transparent || polygon.StrokeColor == nil || polygon.StrokeWidth == 0)) || polygon.Sides < 3 { + return + } + size := polygon.Size() + + // Vertex: BEG + bounds, points := p.vecRectCoords(pos, polygon, frame, 0.0) + program := p.polygonProgram + p.ctx.UseProgram(program.ref) + p.updateBuffer(program.buff, points) + p.UpdateVertexArray(program, "vert", 2, 4, 0) + p.UpdateVertexArray(program, "normal", 2, 4, 2) + + p.ctx.BlendFunc(srcAlpha, oneMinusSrcAlpha) + p.logError() + // Vertex: END + + // Fragment: BEG + frameWidthScaled, frameHeightScaled := p.scaleFrameSize(frame) + p.SetUniform2f(program, "frame_size", frameWidthScaled, frameHeightScaled) + + x1Scaled, x2Scaled, y1Scaled, y2Scaled := p.scaleRectCoords(bounds[0], bounds[2], bounds[1], bounds[3]) + p.SetUniform4f(program, "rect_coords", x1Scaled, x2Scaled, y1Scaled, y2Scaled) + + edgeSoftnessScaled := roundToPixel(edgeSoftness*p.pixScale, 1.0) + p.SetUniform1f(program, "edge_softness", edgeSoftnessScaled) + + outerRadius := fyne.Min(size.Width, size.Height) / 2 + outerRadiusScaled := roundToPixel(outerRadius*p.pixScale, 1.0) + p.SetUniform1f(program, "outer_radius", outerRadiusScaled) + + p.SetUniform1f(program, "angle", polygon.Angle) + p.SetUniform1f(program, "sides", float32(polygon.Sides)) + + cornerRadius := fyne.Min(paint.GetMaximumRadius(size), polygon.CornerRadius) + cornerRadiusScaled := roundToPixel(cornerRadius*p.pixScale, 1.0) + p.SetUniform1f(program, "corner_radius", cornerRadiusScaled) + + strokeWidthScaled := roundToPixel(polygon.StrokeWidth*p.pixScale, 1.0) + p.SetUniform1f(program, "stroke_width", strokeWidthScaled) + + r, g, b, a := getFragmentColor(polygon.FillColor) + p.SetUniform4f(program, "fill_color", r, g, b, a) + + strokeColor := polygon.StrokeColor + if strokeColor == nil { + strokeColor = color.Transparent + } + r, g, b, a = getFragmentColor(strokeColor) + p.SetUniform4f(program, "stroke_color", r, g, b, a) + + p.logError() + // Fragment: END + + p.ctx.DrawArrays(triangleStrip, 0, 4) + p.logError() +} + +func (p *painter) drawArc(arc *canvas.Arc, pos fyne.Position, frame fyne.Size) { + if ((arc.FillColor == color.Transparent || arc.FillColor == nil) && (arc.StrokeColor == color.Transparent || arc.StrokeColor == nil || arc.StrokeWidth == 0)) || arc.StartAngle == arc.EndAngle { + return + } + + // Vertex: BEG + bounds, points := p.vecRectCoords(pos, arc, frame, 0.0) + program := p.arcProgram + p.ctx.UseProgram(program.ref) + p.updateBuffer(program.buff, points) + p.UpdateVertexArray(program, "vert", 2, 4, 0) + p.UpdateVertexArray(program, "normal", 2, 4, 2) + + p.ctx.BlendFunc(srcAlpha, oneMinusSrcAlpha) + p.logError() + // Vertex: END + + // Fragment: BEG + frameWidthScaled, frameHeightScaled := p.scaleFrameSize(frame) + p.SetUniform2f(program, "frame_size", frameWidthScaled, frameHeightScaled) + + x1Scaled, x2Scaled, y1Scaled, y2Scaled := p.scaleRectCoords(bounds[0], bounds[2], bounds[1], bounds[3]) + p.SetUniform4f(program, "rect_coords", x1Scaled, x2Scaled, y1Scaled, y2Scaled) + + edgeSoftnessScaled := roundToPixel(edgeSoftness*p.pixScale, 1.0) + p.SetUniform1f(program, "edge_softness", edgeSoftnessScaled) + + outerRadius := fyne.Min(arc.Size().Width, arc.Size().Height) / 2 + outerRadiusScaled := roundToPixel(outerRadius*p.pixScale, 1.0) + p.SetUniform1f(program, "outer_radius", outerRadiusScaled) + + innerRadius := outerRadius * float32(math.Min(1.0, math.Max(0.0, float64(arc.CutoutRatio)))) + innerRadiusScaled := roundToPixel(innerRadius*p.pixScale, 1.0) + p.SetUniform1f(program, "inner_radius", innerRadiusScaled) + + startAngle, endAngle := paint.NormalizeArcAngles(arc.StartAngle, arc.EndAngle) + p.SetUniform1f(program, "start_angle", startAngle) + p.SetUniform1f(program, "end_angle", endAngle) + + cornerRadius := fyne.Min(paint.GetMaximumRadiusArc(outerRadius, innerRadius, arc.EndAngle-arc.StartAngle), arc.CornerRadius) + cornerRadiusScaled := roundToPixel(cornerRadius*p.pixScale, 1.0) + p.SetUniform1f(program, "corner_radius", cornerRadiusScaled) + + strokeWidthScaled := roundToPixel(arc.StrokeWidth*p.pixScale, 1.0) + p.SetUniform1f(program, "stroke_width", strokeWidthScaled) + + r, g, b, a := getFragmentColor(arc.FillColor) + p.SetUniform4f(program, "fill_color", r, g, b, a) + + strokeColor := arc.StrokeColor + if strokeColor == nil { + strokeColor = color.Transparent + } + r, g, b, a = getFragmentColor(strokeColor) + p.SetUniform4f(program, "stroke_color", r, g, b, a) + + p.logError() + // Fragment: END + + p.ctx.DrawArrays(triangleStrip, 0, 4) + p.logError() +} + +func (p *painter) drawText(text *canvas.Text, pos fyne.Position, frame fyne.Size) { + if text.Text == "" || text.Text == " " { + return + } + + size := text.MinSize() + containerSize := text.Size() + switch text.Alignment { + case fyne.TextAlignTrailing: + pos = fyne.NewPos(pos.X+containerSize.Width-size.Width, pos.Y) + case fyne.TextAlignCenter: + pos = fyne.NewPos(pos.X+(containerSize.Width-size.Width)/2, pos.Y) + } + + if containerSize.Height > size.Height { + pos = fyne.NewPos(pos.X, pos.Y+(containerSize.Height-size.Height)/2) + } + + // text size is sensitive to position on screen + size, _ = roundToPixelCoords(size, text.Position(), p.pixScale) + size.Width += roundToPixel(paint.VectorPad(text), p.pixScale) + p.drawTextureWithDetails(text, p.newGlTextTexture, pos, size, frame, canvas.ImageFillStretch, 1.0, 0) +} + +func (p *painter) drawTextureWithDetails(o fyne.CanvasObject, creator func(canvasObject fyne.CanvasObject) Texture, + pos fyne.Position, size, frame fyne.Size, fill canvas.ImageFill, alpha float32, pad float32, +) { + texture, err := p.getTexture(o, creator) + if err != nil { + return + } + + cornerRadius := float32(0) + aspect := float32(0) + if img, ok := o.(*canvas.Image); ok { + aspect = img.Aspect() + if aspect == 0 { + aspect = 1 // fallback, should not occur - normally an image load error + } + if img.CornerRadius > 0 { + cornerRadius = img.CornerRadius + } + } + points, insets := p.rectCoords(size, pos, frame, fill, aspect, pad) + inner, _ := rectInnerCoords(size, pos, fill, aspect) + + p.ctx.UseProgram(p.program.ref) + p.updateBuffer(p.program.buff, points) + p.UpdateVertexArray(p.program, "vert", 3, 5, 0) + p.UpdateVertexArray(p.program, "vertTexCoord", 2, 5, 3) + + // Set corner radius and texture size in pixels + cornerRadius = fyne.Min(paint.GetMaximumRadius(size), cornerRadius) + p.SetUniform1f(p.program, "cornerRadius", cornerRadius*p.pixScale) + p.SetUniform2f(p.program, "size", inner.Width*p.pixScale, inner.Height*p.pixScale) + p.SetUniform4f(p.program, "inset", insets[0], insets[1], insets[2], insets[3]) // texture coordinate insets (minX, minY, maxX, maxY) + + p.SetUniform1f(p.program, "alpha", alpha) + + p.ctx.BlendFunc(one, oneMinusSrcAlpha) + p.logError() + + p.ctx.ActiveTexture(texture0) + p.ctx.BindTexture(texture2D, texture) + p.logError() + + p.ctx.DrawArrays(triangleStrip, 0, 4) + p.logError() +} + +func (p *painter) lineCoords(pos, pos1, pos2 fyne.Position, lineWidth, feather float32, frame fyne.Size) ([]float32, float32, float32) { + // Shift line coordinates so that they match the target position. + xPosDiff := pos.X - fyne.Min(pos1.X, pos2.X) + yPosDiff := pos.Y - fyne.Min(pos1.Y, pos2.Y) + pos1.X = roundToPixel(pos1.X+xPosDiff, p.pixScale) + pos1.Y = roundToPixel(pos1.Y+yPosDiff, p.pixScale) + pos2.X = roundToPixel(pos2.X+xPosDiff, p.pixScale) + pos2.Y = roundToPixel(pos2.Y+yPosDiff, p.pixScale) + + if lineWidth <= 1 { + offset := float32(0.5) // adjust location for lines < 1pt on regular display + if lineWidth <= 0.5 && p.pixScale > 1 { // and for 1px drawing on HiDPI (width 0.5) + offset = 0.25 + } + if pos1.X == pos2.X { + pos1.X -= offset + pos2.X -= offset + } + if pos1.Y == pos2.Y { + pos1.Y -= offset + pos2.Y -= offset + } + } + + x1Pos := pos1.X / frame.Width + x1 := -1 + x1Pos*2 + y1Pos := pos1.Y / frame.Height + y1 := 1 - y1Pos*2 + x2Pos := pos2.X / frame.Width + x2 := -1 + x2Pos*2 + y2Pos := pos2.Y / frame.Height + y2 := 1 - y2Pos*2 + + normalX := (pos2.Y - pos1.Y) / frame.Width + normalY := (pos2.X - pos1.X) / frame.Height + dirLength := float32(math.Sqrt(float64(normalX*normalX + normalY*normalY))) + normalX /= dirLength + normalY /= dirLength + + normalObjX := normalX * 0.5 * frame.Width + normalObjY := normalY * 0.5 * frame.Height + widthMultiplier := float32(math.Sqrt(float64(normalObjX*normalObjX + normalObjY*normalObjY))) + halfWidth := (roundToPixel(lineWidth+feather, p.pixScale) * 0.5) / widthMultiplier + featherWidth := feather / widthMultiplier + + return []float32{ + // coord x, y normal x, y + x1, y1, normalX, normalY, + x2, y2, normalX, normalY, + x2, y2, -normalX, -normalY, + x2, y2, -normalX, -normalY, + x1, y1, normalX, normalY, + x1, y1, -normalX, -normalY, + }, halfWidth, featherWidth +} + +// rectCoords calculates the openGL coordinate space of a rectangle +func (p *painter) rectCoords(size fyne.Size, pos fyne.Position, frame fyne.Size, + fill canvas.ImageFill, aspect float32, pad float32, +) ([]float32, [4]float32) { + size, pos = rectInnerCoords(size, pos, fill, aspect) + size, pos = roundToPixelCoords(size, pos, p.pixScale) + + xPos := (pos.X - pad) / frame.Width + x1 := -1 + xPos*2 + x2Pos := (pos.X + size.Width + pad) / frame.Width + x2 := -1 + x2Pos*2 + + yPos := (pos.Y - pad) / frame.Height + y1 := 1 - yPos*2 + y2Pos := (pos.Y + size.Height + pad) / frame.Height + y2 := 1 - y2Pos*2 + + xInset := float32(0.0) + yInset := float32(0.0) + + if fill == canvas.ImageFillCover { + viewAspect := size.Width / size.Height + + if viewAspect > aspect { + newHeight := size.Width / aspect + heightPad := (newHeight - size.Height) / 2 + yInset = heightPad / newHeight + } else if viewAspect < aspect { + newWidth := size.Height * aspect + widthPad := (newWidth - size.Width) / 2 + xInset = widthPad / newWidth + } + } + + insets := [4]float32{xInset, yInset, 1.0 - xInset, 1.0 - yInset} + + return []float32{ + // coord x, y, z texture x, y + x1, y2, 0, insets[0], insets[3], // top left + x1, y1, 0, insets[0], insets[1], // bottom left + x2, y2, 0, insets[2], insets[3], // top right + x2, y1, 0, insets[2], insets[1], // bottom right + }, insets +} + +func rectInnerCoords(size fyne.Size, pos fyne.Position, fill canvas.ImageFill, aspect float32) (fyne.Size, fyne.Position) { + if fill == canvas.ImageFillContain || fill == canvas.ImageFillOriginal { + // change pos and size accordingly + + viewAspect := size.Width / size.Height + + newWidth, newHeight := size.Width, size.Height + widthPad, heightPad := float32(0), float32(0) + if viewAspect > aspect { + newWidth = size.Height * aspect + widthPad = (size.Width - newWidth) / 2 + } else if viewAspect < aspect { + newHeight = size.Width / aspect + heightPad = (size.Height - newHeight) / 2 + } + + return fyne.NewSize(newWidth, newHeight), fyne.NewPos(pos.X+widthPad, pos.Y+heightPad) + } + + return size, pos +} + +func (p *painter) vecRectCoords(pos fyne.Position, rect fyne.CanvasObject, frame fyne.Size, aspect float32) ([4]float32, []float32) { + xPad, yPad := float32(0), float32(0) + + if aspect != 0 { + inner := rect.Size() + frameAspect := inner.Width / inner.Height + + if frameAspect > aspect { + newWidth := inner.Height * aspect + xPad = (inner.Width - newWidth) / 2 + } else if frameAspect < aspect { + newHeight := inner.Width / aspect + yPad = (inner.Height - newHeight) / 2 + } + } + + return p.vecRectCoordsWithPad(pos, rect, frame, xPad, yPad) +} + +func (p *painter) vecRectCoordsWithPad(pos fyne.Position, rect fyne.CanvasObject, frame fyne.Size, xPad, yPad float32) ([4]float32, []float32) { + size := rect.Size() + pos1 := rect.Position() + + xPosDiff := pos.X - pos1.X + xPad + yPosDiff := pos.Y - pos1.Y + yPad + pos1.X = roundToPixel(pos1.X+xPosDiff, p.pixScale) + pos1.Y = roundToPixel(pos1.Y+yPosDiff, p.pixScale) + size.Width = roundToPixel(size.Width-2*xPad, p.pixScale) + size.Height = roundToPixel(size.Height-2*yPad, p.pixScale) + + // without edge softness adjustment the rectangle has cropped edges + edgeSoftnessScaled := roundToPixel(edgeSoftness*p.pixScale, 1.0) + x1Pos := pos1.X + x1Norm := -1 + (x1Pos-edgeSoftnessScaled)*2/frame.Width + x2Pos := pos1.X + size.Width + x2Norm := -1 + (x2Pos+edgeSoftnessScaled)*2/frame.Width + y1Pos := pos1.Y + y1Norm := 1 - (y1Pos-edgeSoftnessScaled)*2/frame.Height + y2Pos := pos1.Y + size.Height + y2Norm := 1 - (y2Pos+edgeSoftnessScaled)*2/frame.Height + + // output a norm for the fill and the vert is unused, but we pass 0 to avoid optimisation issues + coords := []float32{ + 0, 0, x1Norm, y1Norm, // first triangle + 0, 0, x2Norm, y1Norm, // second triangle + 0, 0, x1Norm, y2Norm, + 0, 0, x2Norm, y2Norm, + } + + return [4]float32{x1Pos, y1Pos, x2Pos, y2Pos}, coords +} + +func (p *painter) vecSquareCoords(pos fyne.Position, rect fyne.CanvasObject, frame fyne.Size) ([4]float32, []float32) { + return p.vecRectCoordsWithPad(pos, rect, frame, 0, 0) +} + +func roundToPixel(v float32, pixScale float32) float32 { + if pixScale == 1.0 { + return float32(math.Round(float64(v))) + } + + return float32(math.Round(float64(v*pixScale))) / pixScale +} + +func roundToPixelCoords(size fyne.Size, pos fyne.Position, pixScale float32) (fyne.Size, fyne.Position) { + end := pos.Add(size) + end.X = roundToPixel(end.X, pixScale) + end.Y = roundToPixel(end.Y, pixScale) + pos.X = roundToPixel(pos.X, pixScale) + pos.Y = roundToPixel(pos.Y, pixScale) + size.Width = end.X - pos.X + size.Height = end.Y - pos.Y + + return size, pos +} + +// Returns FragmentColor(red,green,blue,alpha) from fyne.Color +func getFragmentColor(col color.Color) (float32, float32, float32, float32) { + if col == nil { + return 0, 0, 0, 0 + } + r, g, b, a := col.RGBA() + if a == 0 { + return 0, 0, 0, 0 + } + alpha := float32(a) + return float32(r) / alpha, float32(g) / alpha, float32(b) / alpha, alpha / 0xffff +} + +func (p *painter) scaleFrameSize(frame fyne.Size) (float32, float32) { + frameWidthScaled := roundToPixel(frame.Width*p.pixScale, 1.0) + frameHeightScaled := roundToPixel(frame.Height*p.pixScale, 1.0) + return frameWidthScaled, frameHeightScaled +} + +// Returns scaled RectCoords(x1,x2,y1,y2) in same order +func (p *painter) scaleRectCoords(x1, x2, y1, y2 float32) (float32, float32, float32, float32) { + x1Scaled := roundToPixel(x1*p.pixScale, 1.0) + x2Scaled := roundToPixel(x2*p.pixScale, 1.0) + y1Scaled := roundToPixel(y1*p.pixScale, 1.0) + y2Scaled := roundToPixel(y2*p.pixScale, 1.0) + return x1Scaled, x2Scaled, y1Scaled, y2Scaled +} diff --git a/vendor/fyne.io/fyne/v2/internal/painter/gl/draw_desktop.go b/vendor/fyne.io/fyne/v2/internal/painter/gl/draw_desktop.go new file mode 100644 index 0000000..e0f4022 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/painter/gl/draw_desktop.go @@ -0,0 +1,12 @@ +//go:build windows || darwin || linux || openbsd || freebsd + +package gl + +func (p *painter) updateBuffer(vbo Buffer, points []float32) { + p.ctx.BindBuffer(arrayBuffer, vbo) + p.logError() + // BufferSubData seems significantly less performant on desktop + // so use BufferData instead + p.ctx.BufferData(arrayBuffer, points, staticDraw) + p.logError() +} diff --git a/vendor/fyne.io/fyne/v2/internal/painter/gl/draw_notdesktop.go b/vendor/fyne.io/fyne/v2/internal/painter/gl/draw_notdesktop.go new file mode 100644 index 0000000..3b7f511 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/painter/gl/draw_notdesktop.go @@ -0,0 +1,10 @@ +//go:build !(windows || darwin || linux || openbsd || freebsd) + +package gl + +func (p *painter) updateBuffer(vbo Buffer, points []float32) { + p.ctx.BindBuffer(arrayBuffer, vbo) + p.logError() + p.ctx.BufferSubData(arrayBuffer, points) + p.logError() +} diff --git a/vendor/fyne.io/fyne/v2/internal/painter/gl/gl.go b/vendor/fyne.io/fyne/v2/internal/painter/gl/gl.go new file mode 100644 index 0000000..e8f422f --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/painter/gl/gl.go @@ -0,0 +1,35 @@ +package gl + +import ( + "log" + "runtime" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/build" +) + +const ( + floatSize = 4 + max16bit = float32(255 * 255) +) + +// logGLError logs error in the GL renderer. +// +// Receives a function as parameter, to lazily get the error code only when +// needed, avoiding unneeded overhead. +func logGLError(getError func() uint32) { + if build.Mode != fyne.BuildDebug { + return + } + + err := getError() + if err == 0 { + return + } + + log.Printf("Error %x in GL Renderer", err) + _, file, line, ok := runtime.Caller(2) + if ok { + log.Printf(" At: %s:%d", file, line) + } +} diff --git a/vendor/fyne.io/fyne/v2/internal/painter/gl/gl_core.go b/vendor/fyne.io/fyne/v2/internal/painter/gl/gl_core.go new file mode 100644 index 0000000..44bce50 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/painter/gl/gl_core.go @@ -0,0 +1,360 @@ +//go:build (!gles && !arm && !arm64 && !android && !ios && !mobile && !test_web_driver && !wasm) || (darwin && !mobile && !ios && !wasm && !test_web_driver) + +package gl + +import ( + "strings" + + "github.com/go-gl/gl/v2.1/gl" + + "fyne.io/fyne/v2" +) + +const ( + arrayBuffer = gl.ARRAY_BUFFER + bitColorBuffer = gl.COLOR_BUFFER_BIT + bitDepthBuffer = gl.DEPTH_BUFFER_BIT + clampToEdge = gl.CLAMP_TO_EDGE + colorFormatRGBA = gl.RGBA + compileStatus = gl.COMPILE_STATUS + constantAlpha = gl.CONSTANT_ALPHA + float = gl.FLOAT + fragmentShader = gl.FRAGMENT_SHADER + front = gl.FRONT + glFalse = gl.FALSE + linkStatus = gl.LINK_STATUS + one = gl.ONE + oneMinusConstantAlpha = gl.ONE_MINUS_CONSTANT_ALPHA + oneMinusSrcAlpha = gl.ONE_MINUS_SRC_ALPHA + scissorTest = gl.SCISSOR_TEST + srcAlpha = gl.SRC_ALPHA + staticDraw = gl.STATIC_DRAW + texture0 = gl.TEXTURE0 + texture2D = gl.TEXTURE_2D + textureMinFilter = gl.TEXTURE_MIN_FILTER + textureMagFilter = gl.TEXTURE_MAG_FILTER + textureWrapS = gl.TEXTURE_WRAP_S + textureWrapT = gl.TEXTURE_WRAP_T + triangles = gl.TRIANGLES + triangleStrip = gl.TRIANGLE_STRIP + unsignedByte = gl.UNSIGNED_BYTE + vertexShader = gl.VERTEX_SHADER +) + +const ( + noBuffer = Buffer(0) + noShader = Shader(0) +) + +type ( + // Attribute represents a GL attribute + Attribute int32 + // Buffer represents a GL buffer + Buffer uint32 + // Program represents a compiled GL program + Program uint32 + // Shader represents a GL shader + Shader uint32 + // Uniform represents a GL uniform + Uniform int32 +) + +var textureFilterToGL = [...]int32{gl.LINEAR, gl.NEAREST, gl.LINEAR} + +func (p *painter) Init() { + p.ctx = &coreContext{} + err := gl.Init() + if err != nil { + fyne.LogError("failed to initialise OpenGL", err) + return + } + + gl.Disable(gl.DEPTH_TEST) + gl.Enable(gl.BLEND) + p.logError() + p.program = ProgramState{ + ref: p.createProgram("simple"), + buff: p.createBuffer(20), + uniforms: make(map[string]*UniformState), + attributes: make(map[string]Attribute), + } + p.getUniformLocations(p.program, "text", "alpha", "cornerRadius", "size", "inset") + p.enableAttribArrays(p.program, "vert", "vertTexCoord") + + p.lineProgram = ProgramState{ + ref: p.createProgram("line"), + buff: p.createBuffer(24), + uniforms: make(map[string]*UniformState), + attributes: make(map[string]Attribute), + } + p.getUniformLocations(p.lineProgram, "feather", "color", "lineWidth") + p.enableAttribArrays(p.lineProgram, "vert", "normal") + + p.rectangleProgram = ProgramState{ + ref: p.createProgram("rectangle"), + buff: p.createBuffer(16), + uniforms: make(map[string]*UniformState), + attributes: make(map[string]Attribute), + } + p.getUniformLocations( + p.rectangleProgram, + "frame_size", "rect_coords", "stroke_width", "fill_color", "stroke_color", + ) + p.enableAttribArrays(p.rectangleProgram, "vert", "normal") + + p.roundRectangleProgram = ProgramState{ + ref: p.createProgram("round_rectangle"), + buff: p.createBuffer(16), + uniforms: make(map[string]*UniformState), + attributes: make(map[string]Attribute), + } + p.getUniformLocations(p.roundRectangleProgram, + "frame_size", "rect_coords", + "stroke_width_half", "rect_size_half", + "radius", "edge_softness", + "fill_color", "stroke_color", + ) + p.enableAttribArrays(p.roundRectangleProgram, "vert", "normal") + + p.polygonProgram = ProgramState{ + ref: p.createProgram("polygon"), + buff: p.createBuffer(16), + uniforms: make(map[string]*UniformState), + attributes: make(map[string]Attribute), + } + p.getUniformLocations(p.polygonProgram, + "frame_size", "rect_coords", "edge_softness", + "outer_radius", "angle", "sides", + "fill_color", "corner_radius", + "stroke_width", "stroke_color", + ) + p.enableAttribArrays(p.polygonProgram, "vert", "normal") + + p.arcProgram = ProgramState{ + ref: p.createProgram("arc"), + buff: p.createBuffer(16), + uniforms: make(map[string]*UniformState), + attributes: make(map[string]Attribute), + } + p.getUniformLocations(p.arcProgram, + "frame_size", "rect_coords", + "inner_radius", "outer_radius", + "start_angle", "end_angle", + "edge_softness", "corner_radius", + "stroke_width", "stroke_color", + "fill_color", + ) + p.enableAttribArrays(p.arcProgram, "vert", "normal") +} + +func (p *painter) getUniformLocations(pState ProgramState, names ...string) { + for _, name := range names { + u := p.ctx.GetUniformLocation(pState.ref, name) + pState.uniforms[name] = &UniformState{ref: u} + } +} + +func (p *painter) enableAttribArrays(pState ProgramState, names ...string) { + for _, name := range names { + a := p.ctx.GetAttribLocation(pState.ref, name) + p.ctx.EnableVertexAttribArray(a) + pState.attributes[name] = a + } +} + +type coreContext struct{} + +var _ context = (*coreContext)(nil) + +func (c *coreContext) ActiveTexture(textureUnit uint32) { + gl.ActiveTexture(textureUnit) +} + +func (c *coreContext) AttachShader(program Program, shader Shader) { + gl.AttachShader(uint32(program), uint32(shader)) +} + +func (c *coreContext) BindBuffer(target uint32, buf Buffer) { + gl.BindBuffer(target, uint32(buf)) +} + +func (c *coreContext) BindTexture(target uint32, texture Texture) { + gl.BindTexture(target, uint32(texture)) +} + +func (c *coreContext) BlendColor(r, g, b, a float32) { + gl.BlendColor(r, g, b, a) +} + +func (c *coreContext) BlendFunc(srcFactor, destFactor uint32) { + gl.BlendFunc(srcFactor, destFactor) +} + +func (c *coreContext) BufferData(target uint32, points []float32, usage uint32) { + gl.BufferData(target, 4*len(points), gl.Ptr(points), usage) +} + +func (c *coreContext) BufferSubData(target uint32, points []float32) { + gl.BufferSubData(target, 0, 4*len(points), gl.Ptr(points)) +} + +func (c *coreContext) Clear(mask uint32) { + gl.Clear(mask) +} + +func (c *coreContext) ClearColor(r, g, b, a float32) { + gl.ClearColor(r, g, b, a) +} + +func (c *coreContext) CompileShader(shader Shader) { + gl.CompileShader(uint32(shader)) +} + +func (c *coreContext) CreateBuffer() Buffer { + var vbo uint32 + gl.GenBuffers(1, &vbo) + return Buffer(vbo) +} + +func (c *coreContext) CreateProgram() Program { + return Program(gl.CreateProgram()) +} + +func (c *coreContext) CreateShader(typ uint32) Shader { + return Shader(gl.CreateShader(typ)) +} + +func (c *coreContext) CreateTexture() (texture Texture) { + var tex uint32 + gl.GenTextures(1, &tex) + return Texture(tex) +} + +func (c *coreContext) DeleteBuffer(buffer Buffer) { + gl.DeleteBuffers(1, (*uint32)(&buffer)) +} + +func (c *coreContext) DeleteTexture(texture Texture) { + tex := uint32(texture) + gl.DeleteTextures(1, &tex) +} + +func (c *coreContext) Disable(capability uint32) { + gl.Disable(capability) +} + +func (c *coreContext) DrawArrays(mode uint32, first, count int) { + gl.DrawArrays(mode, int32(first), int32(count)) +} + +func (c *coreContext) Enable(capability uint32) { + gl.Enable(capability) +} + +func (c *coreContext) EnableVertexAttribArray(attribute Attribute) { + gl.EnableVertexAttribArray(uint32(attribute)) +} + +func (c *coreContext) GetAttribLocation(program Program, name string) Attribute { + return Attribute(gl.GetAttribLocation(uint32(program), gl.Str(name+"\x00"))) +} + +func (c *coreContext) GetError() uint32 { + return gl.GetError() +} + +func (c *coreContext) GetProgrami(program Program, param uint32) int { + var value int32 + gl.GetProgramiv(uint32(program), param, &value) + return int(value) +} + +func (c *coreContext) GetProgramInfoLog(program Program) string { + var logLength int32 + gl.GetProgramiv(uint32(program), gl.INFO_LOG_LENGTH, &logLength) + info := strings.Repeat("\x00", int(logLength+1)) + gl.GetProgramInfoLog(uint32(program), logLength, nil, gl.Str(info)) + return info +} + +func (c *coreContext) GetShaderi(shader Shader, param uint32) int { + var value int32 + gl.GetShaderiv(uint32(shader), param, &value) + return int(value) +} + +func (c *coreContext) GetShaderInfoLog(shader Shader) string { + var logLength int32 + gl.GetShaderiv(uint32(shader), gl.INFO_LOG_LENGTH, &logLength) + info := strings.Repeat("\x00", int(logLength+1)) + gl.GetShaderInfoLog(uint32(shader), logLength, nil, gl.Str(info)) + return info +} + +func (c *coreContext) GetUniformLocation(program Program, name string) Uniform { + return Uniform(gl.GetUniformLocation(uint32(program), gl.Str(name+"\x00"))) +} + +func (c *coreContext) LinkProgram(program Program) { + gl.LinkProgram(uint32(program)) +} + +func (c *coreContext) ReadBuffer(src uint32) { + gl.ReadBuffer(src) +} + +func (c *coreContext) ReadPixels(x, y, width, height int, colorFormat, typ uint32, pixels []uint8) { + gl.ReadPixels(int32(x), int32(y), int32(width), int32(height), colorFormat, typ, gl.Ptr(pixels)) +} + +func (c *coreContext) Scissor(x, y, w, h int32) { + gl.Scissor(x, y, w, h) +} + +func (c *coreContext) ShaderSource(shader Shader, source string) { + csources, free := gl.Strs(source + "\x00") + defer free() + gl.ShaderSource(uint32(shader), 1, csources, nil) +} + +func (c *coreContext) TexImage2D(target uint32, level, width, height int, colorFormat, typ uint32, data []uint8) { + gl.TexImage2D( + target, + int32(level), + int32(colorFormat), + int32(width), + int32(height), + 0, + colorFormat, + typ, + gl.Ptr(data), + ) +} + +func (c *coreContext) TexParameteri(target, param uint32, value int32) { + gl.TexParameteri(target, param, value) +} + +func (c *coreContext) Uniform1f(uniform Uniform, v float32) { + gl.Uniform1f(int32(uniform), v) +} + +func (c *coreContext) Uniform2f(uniform Uniform, v0, v1 float32) { + gl.Uniform2f(int32(uniform), v0, v1) +} + +func (c *coreContext) Uniform4f(uniform Uniform, v0, v1, v2, v3 float32) { + gl.Uniform4f(int32(uniform), v0, v1, v2, v3) +} + +func (c *coreContext) UseProgram(program Program) { + gl.UseProgram(uint32(program)) +} + +func (c *coreContext) VertexAttribPointerWithOffset(attribute Attribute, size int, typ uint32, normalized bool, stride, offset int) { + gl.VertexAttribPointerWithOffset(uint32(attribute), int32(size), typ, normalized, int32(stride), uintptr(offset)) +} + +func (c *coreContext) Viewport(x, y, width, height int) { + gl.Viewport(int32(x), int32(y), int32(width), int32(height)) +} diff --git a/vendor/fyne.io/fyne/v2/internal/painter/gl/gl_es.go b/vendor/fyne.io/fyne/v2/internal/painter/gl/gl_es.go new file mode 100644 index 0000000..ca15b29 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/painter/gl/gl_es.go @@ -0,0 +1,360 @@ +//go:build (gles || arm || arm64) && !android && !ios && !mobile && !darwin && !wasm && !test_web_driver + +package gl + +import ( + "strings" + + gl "github.com/go-gl/gl/v3.1/gles2" + + "fyne.io/fyne/v2" +) + +const ( + arrayBuffer = gl.ARRAY_BUFFER + bitColorBuffer = gl.COLOR_BUFFER_BIT + bitDepthBuffer = gl.DEPTH_BUFFER_BIT + clampToEdge = gl.CLAMP_TO_EDGE + colorFormatRGBA = gl.RGBA + compileStatus = gl.COMPILE_STATUS + constantAlpha = gl.CONSTANT_ALPHA + float = gl.FLOAT + fragmentShader = gl.FRAGMENT_SHADER + front = gl.FRONT + glFalse = gl.FALSE + linkStatus = gl.LINK_STATUS + one = gl.ONE + oneMinusConstantAlpha = gl.ONE_MINUS_CONSTANT_ALPHA + oneMinusSrcAlpha = gl.ONE_MINUS_SRC_ALPHA + scissorTest = gl.SCISSOR_TEST + srcAlpha = gl.SRC_ALPHA + staticDraw = gl.STATIC_DRAW + texture0 = gl.TEXTURE0 + texture2D = gl.TEXTURE_2D + textureMinFilter = gl.TEXTURE_MIN_FILTER + textureMagFilter = gl.TEXTURE_MAG_FILTER + textureWrapS = gl.TEXTURE_WRAP_S + textureWrapT = gl.TEXTURE_WRAP_T + triangles = gl.TRIANGLES + triangleStrip = gl.TRIANGLE_STRIP + unsignedByte = gl.UNSIGNED_BYTE + vertexShader = gl.VERTEX_SHADER +) + +const ( + noBuffer = Buffer(0) + noShader = Shader(0) +) + +type ( + // Attribute represents a GL attribute + Attribute int32 + // Buffer represents a GL buffer + Buffer uint32 + // Program represents a compiled GL program + Program uint32 + // Shader represents a GL shader + Shader uint32 + // Uniform represents a GL uniform + Uniform int32 +) + +var textureFilterToGL = [...]int32{gl.LINEAR, gl.NEAREST, gl.LINEAR} + +func (p *painter) Init() { + p.ctx = &esContext{} + err := gl.Init() + if err != nil { + fyne.LogError("failed to initialise OpenGL", err) + return + } + + gl.Disable(gl.DEPTH_TEST) + gl.Enable(gl.BLEND) + p.logError() + p.program = ProgramState{ + ref: p.createProgram("simple_es"), + buff: p.createBuffer(20), + uniforms: make(map[string]*UniformState), + attributes: make(map[string]Attribute), + } + p.getUniformLocations(p.program, "text", "alpha", "cornerRadius", "size", "inset") + p.enableAttribArrays(p.program, "vert", "vertTexCoord") + + p.lineProgram = ProgramState{ + ref: p.createProgram("line_es"), + buff: p.createBuffer(24), + uniforms: make(map[string]*UniformState), + attributes: make(map[string]Attribute), + } + p.getUniformLocations(p.lineProgram, "color", "feather", "lineWidth") + p.enableAttribArrays(p.lineProgram, "vert", "normal") + + p.rectangleProgram = ProgramState{ + ref: p.createProgram("rectangle_es"), + buff: p.createBuffer(16), + uniforms: make(map[string]*UniformState), + attributes: make(map[string]Attribute), + } + p.getUniformLocations( + p.rectangleProgram, + "frame_size", "rect_coords", "stroke_width", "fill_color", "stroke_color", + ) + p.enableAttribArrays(p.rectangleProgram, "vert", "normal") + + p.roundRectangleProgram = ProgramState{ + ref: p.createProgram("round_rectangle_es"), + buff: p.createBuffer(16), + uniforms: make(map[string]*UniformState), + attributes: make(map[string]Attribute), + } + p.getUniformLocations(p.roundRectangleProgram, + "frame_size", "rect_coords", + "stroke_width_half", "rect_size_half", + "radius", "edge_softness", + "fill_color", "stroke_color", + ) + p.enableAttribArrays(p.roundRectangleProgram, "vert", "normal") + + p.polygonProgram = ProgramState{ + ref: p.createProgram("polygon_es"), + buff: p.createBuffer(16), + uniforms: make(map[string]*UniformState), + attributes: make(map[string]Attribute), + } + p.getUniformLocations(p.polygonProgram, + "frame_size", "rect_coords", "edge_softness", + "outer_radius", "angle", "sides", + "fill_color", "corner_radius", + "stroke_width", "stroke_color", + ) + p.enableAttribArrays(p.polygonProgram, "vert", "normal") + + p.arcProgram = ProgramState{ + ref: p.createProgram("arc_es"), + buff: p.createBuffer(16), + uniforms: make(map[string]*UniformState), + attributes: make(map[string]Attribute), + } + p.getUniformLocations(p.arcProgram, + "frame_size", "rect_coords", + "inner_radius", "outer_radius", + "start_angle", "end_angle", + "edge_softness", "corner_radius", + "stroke_width", "stroke_color", + "fill_color", + ) + p.enableAttribArrays(p.arcProgram, "vert", "normal") +} + +func (p *painter) getUniformLocations(pState ProgramState, names ...string) { + for _, name := range names { + u := p.ctx.GetUniformLocation(pState.ref, name) + pState.uniforms[name] = &UniformState{ref: u} + } +} + +func (p *painter) enableAttribArrays(pState ProgramState, names ...string) { + for _, name := range names { + a := p.ctx.GetAttribLocation(pState.ref, name) + p.ctx.EnableVertexAttribArray(a) + pState.attributes[name] = a + } +} + +type esContext struct{} + +var _ context = (*esContext)(nil) + +func (c *esContext) ActiveTexture(textureUnit uint32) { + gl.ActiveTexture(textureUnit) +} + +func (c *esContext) AttachShader(program Program, shader Shader) { + gl.AttachShader(uint32(program), uint32(shader)) +} + +func (c *esContext) BindBuffer(target uint32, buf Buffer) { + gl.BindBuffer(target, uint32(buf)) +} + +func (c *esContext) BindTexture(target uint32, texture Texture) { + gl.BindTexture(target, uint32(texture)) +} + +func (c *esContext) BlendColor(r, g, b, a float32) { + gl.BlendColor(r, g, b, a) +} + +func (c *esContext) BlendFunc(srcFactor, destFactor uint32) { + gl.BlendFunc(srcFactor, destFactor) +} + +func (c *esContext) BufferData(target uint32, points []float32, usage uint32) { + gl.BufferData(target, 4*len(points), gl.Ptr(points), usage) +} + +func (c *esContext) BufferSubData(target uint32, points []float32) { + gl.BufferSubData(target, 0, 4*len(points), gl.Ptr(points)) +} + +func (c *esContext) Clear(mask uint32) { + gl.Clear(mask) +} + +func (c *esContext) ClearColor(r, g, b, a float32) { + gl.ClearColor(r, g, b, a) +} + +func (c *esContext) CompileShader(shader Shader) { + gl.CompileShader(uint32(shader)) +} + +func (c *esContext) CreateBuffer() Buffer { + var vbo uint32 + gl.GenBuffers(1, &vbo) + return Buffer(vbo) +} + +func (c *esContext) CreateProgram() Program { + return Program(gl.CreateProgram()) +} + +func (c *esContext) CreateShader(typ uint32) Shader { + return Shader(gl.CreateShader(typ)) +} + +func (c *esContext) CreateTexture() (texture Texture) { + var tex uint32 + gl.GenTextures(1, &tex) + return Texture(tex) +} + +func (c *esContext) DeleteBuffer(buffer Buffer) { + gl.DeleteBuffers(1, (*uint32)(&buffer)) +} + +func (c *esContext) DeleteTexture(texture Texture) { + tex := uint32(texture) + gl.DeleteTextures(1, &tex) +} + +func (c *esContext) Disable(capability uint32) { + gl.Disable(capability) +} + +func (c *esContext) DrawArrays(mode uint32, first, count int) { + gl.DrawArrays(mode, int32(first), int32(count)) +} + +func (c *esContext) Enable(capability uint32) { + gl.Enable(capability) +} + +func (c *esContext) EnableVertexAttribArray(attribute Attribute) { + gl.EnableVertexAttribArray(uint32(attribute)) +} + +func (c *esContext) GetAttribLocation(program Program, name string) Attribute { + return Attribute(gl.GetAttribLocation(uint32(program), gl.Str(name+"\x00"))) +} + +func (c *esContext) GetError() uint32 { + return gl.GetError() +} + +func (c *esContext) GetProgrami(program Program, param uint32) int { + var value int32 + gl.GetProgramiv(uint32(program), param, &value) + return int(value) +} + +func (c *esContext) GetProgramInfoLog(program Program) string { + var logLength int32 + gl.GetProgramiv(uint32(program), gl.INFO_LOG_LENGTH, &logLength) + info := strings.Repeat("\x00", int(logLength+1)) + gl.GetProgramInfoLog(uint32(program), logLength, nil, gl.Str(info)) + return info +} + +func (c *esContext) GetShaderi(shader Shader, param uint32) int { + var value int32 + gl.GetShaderiv(uint32(shader), param, &value) + return int(value) +} + +func (c *esContext) GetShaderInfoLog(shader Shader) string { + var logLength int32 + gl.GetShaderiv(uint32(shader), gl.INFO_LOG_LENGTH, &logLength) + info := strings.Repeat("\x00", int(logLength+1)) + gl.GetShaderInfoLog(uint32(shader), logLength, nil, gl.Str(info)) + return info +} + +func (c *esContext) GetUniformLocation(program Program, name string) Uniform { + return Uniform(gl.GetUniformLocation(uint32(program), gl.Str(name+"\x00"))) +} + +func (c *esContext) LinkProgram(program Program) { + gl.LinkProgram(uint32(program)) +} + +func (c *esContext) ReadBuffer(src uint32) { + gl.ReadBuffer(src) +} + +func (c *esContext) ReadPixels(x, y, width, height int, colorFormat, typ uint32, pixels []uint8) { + gl.ReadPixels(int32(x), int32(y), int32(width), int32(height), colorFormat, typ, gl.Ptr(pixels)) +} + +func (c *esContext) Scissor(x, y, w, h int32) { + gl.Scissor(x, y, w, h) +} + +func (c *esContext) ShaderSource(shader Shader, source string) { + csources, free := gl.Strs(source + "\x00") + defer free() + gl.ShaderSource(uint32(shader), 1, csources, nil) +} + +func (c *esContext) TexImage2D(target uint32, level, width, height int, colorFormat, typ uint32, data []uint8) { + gl.TexImage2D( + target, + int32(level), + int32(colorFormat), + int32(width), + int32(height), + 0, + colorFormat, + typ, + gl.Ptr(data), + ) +} + +func (c *esContext) TexParameteri(target, param uint32, value int32) { + gl.TexParameteri(target, param, value) +} + +func (c *esContext) Uniform1f(uniform Uniform, v float32) { + gl.Uniform1f(int32(uniform), v) +} + +func (c *esContext) Uniform2f(uniform Uniform, v0, v1 float32) { + gl.Uniform2f(int32(uniform), v0, v1) +} + +func (c *esContext) Uniform4f(uniform Uniform, v0, v1, v2, v3 float32) { + gl.Uniform4f(int32(uniform), v0, v1, v2, v3) +} + +func (c *esContext) UseProgram(program Program) { + gl.UseProgram(uint32(program)) +} + +func (c *esContext) VertexAttribPointerWithOffset(attribute Attribute, size int, typ uint32, normalized bool, stride, offset int) { + gl.VertexAttribPointerWithOffset(uint32(attribute), int32(size), typ, normalized, int32(stride), uintptr(offset)) +} + +func (c *esContext) Viewport(x, y, width, height int) { + gl.Viewport(int32(x), int32(y), int32(width), int32(height)) +} diff --git a/vendor/fyne.io/fyne/v2/internal/painter/gl/gl_gomobile.go b/vendor/fyne.io/fyne/v2/internal/painter/gl/gl_gomobile.go new file mode 100644 index 0000000..e4ecb3d --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/painter/gl/gl_gomobile.go @@ -0,0 +1,368 @@ +//go:build (android || ios || mobile) && (!wasm || !test_web_driver) + +package gl + +import ( + "math" + + "fyne.io/fyne/v2/internal/driver/mobile/gl" +) + +const ( + arrayBuffer = gl.ArrayBuffer + bitColorBuffer = gl.ColorBufferBit + bitDepthBuffer = gl.DepthBufferBit + clampToEdge = gl.ClampToEdge + colorFormatRGBA = gl.RGBA + compileStatus = gl.CompileStatus + constantAlpha = gl.ConstantAlpha + float = gl.Float + fragmentShader = gl.FragmentShader + front = gl.Front + glFalse = gl.False + linkStatus = gl.LinkStatus + one = gl.One + oneMinusConstantAlpha = gl.OneMinusConstantAlpha + oneMinusSrcAlpha = gl.OneMinusSrcAlpha + scissorTest = gl.ScissorTest + srcAlpha = gl.SrcAlpha + staticDraw = gl.StaticDraw + texture0 = gl.Texture0 + texture2D = gl.Texture2D + textureMinFilter = gl.TextureMinFilter + textureMagFilter = gl.TextureMagFilter + textureWrapS = gl.TextureWrapS + textureWrapT = gl.TextureWrapT + triangles = gl.Triangles + triangleStrip = gl.TriangleStrip + unsignedByte = gl.UnsignedByte + vertexShader = gl.VertexShader +) + +type ( + // Attribute represents a GL attribute + Attribute gl.Attrib + // Buffer represents a GL buffer + Buffer gl.Buffer + // Program represents a compiled GL program + Program gl.Program + // Shader represents a GL shader + Shader gl.Shader + // Uniform represents a GL uniform + Uniform gl.Uniform +) + +var ( + compiled []ProgramState // avoid multiple compilations with the re-used mobile GUI context + noBuffer = Buffer{} + noShader = Shader{} + textureFilterToGL = [...]int32{gl.Linear, gl.Nearest} +) + +func (p *painter) glctx() gl.Context { + return p.contextProvider.Context().(gl.Context) +} + +func (p *painter) Init() { + p.ctx = &mobileContext{glContext: p.contextProvider.Context().(gl.Context)} + p.glctx().Disable(gl.DepthTest) + p.glctx().Enable(gl.Blend) + if compiled == nil { + p.program = ProgramState{ + ref: p.createProgram("simple_es"), + buff: p.createBuffer(20), + uniforms: make(map[string]*UniformState), + attributes: make(map[string]Attribute), + } + p.getUniformLocations(p.program, "text", "alpha", "cornerRadius", "size", "inset") + p.enableAttribArrays(p.program, "vert", "vertTexCoord") + + p.lineProgram = ProgramState{ + ref: p.createProgram("line_es"), + buff: p.createBuffer(24), + uniforms: make(map[string]*UniformState), + attributes: make(map[string]Attribute), + } + p.getUniformLocations(p.lineProgram, "color", "feather", "lineWidth") + p.enableAttribArrays(p.lineProgram, "vert", "normal") + + p.rectangleProgram = ProgramState{ + ref: p.createProgram("rectangle_es"), + buff: p.createBuffer(16), + uniforms: make(map[string]*UniformState), + attributes: make(map[string]Attribute), + } + p.getUniformLocations( + p.rectangleProgram, + "frame_size", "rect_coords", "stroke_width", "fill_color", "stroke_color", + ) + p.enableAttribArrays(p.rectangleProgram, "vert", "normal") + + p.roundRectangleProgram = ProgramState{ + ref: p.createProgram("round_rectangle_es"), + buff: p.createBuffer(16), + uniforms: make(map[string]*UniformState), + attributes: make(map[string]Attribute), + } + p.getUniformLocations(p.roundRectangleProgram, + "frame_size", "rect_coords", + "stroke_width_half", "rect_size_half", + "radius", "edge_softness", + "fill_color", "stroke_color", + ) + p.enableAttribArrays(p.roundRectangleProgram, "vert", "normal") + + p.polygonProgram = ProgramState{ + ref: p.createProgram("polygon_es"), + buff: p.createBuffer(16), + uniforms: make(map[string]*UniformState), + attributes: make(map[string]Attribute), + } + p.getUniformLocations(p.polygonProgram, + "frame_size", "rect_coords", "edge_softness", + "outer_radius", "angle", "sides", + "fill_color", "corner_radius", + "stroke_width", "stroke_color", + ) + p.enableAttribArrays(p.polygonProgram, "vert", "normal") + + p.arcProgram = ProgramState{ + ref: p.createProgram("arc_es"), + buff: p.createBuffer(16), + uniforms: make(map[string]*UniformState), + attributes: make(map[string]Attribute), + } + p.getUniformLocations(p.arcProgram, + "frame_size", "rect_coords", + "inner_radius", "outer_radius", + "start_angle", "end_angle", + "edge_softness", "corner_radius", + "stroke_width", "stroke_color", + "fill_color", + ) + p.enableAttribArrays(p.arcProgram, "vert", "normal") + compiled = []ProgramState{ + p.program, + p.lineProgram, + p.rectangleProgram, + p.roundRectangleProgram, + p.polygonProgram, + p.arcProgram, + } + } + + p.program = compiled[0] + p.lineProgram = compiled[1] + p.rectangleProgram = compiled[2] + p.roundRectangleProgram = compiled[3] + p.polygonProgram = compiled[4] + p.arcProgram = compiled[5] +} + +func (p *painter) getUniformLocations(pState ProgramState, names ...string) { + for _, name := range names { + u := p.ctx.GetUniformLocation(pState.ref, name) + pState.uniforms[name] = &UniformState{ref: u} + } +} + +func (p *painter) enableAttribArrays(pState ProgramState, names ...string) { + for _, name := range names { + a := p.ctx.GetAttribLocation(pState.ref, name) + p.ctx.EnableVertexAttribArray(a) + pState.attributes[name] = a + } +} + +type mobileContext struct { + glContext gl.Context +} + +var _ context = (*mobileContext)(nil) + +func (c *mobileContext) ActiveTexture(textureUnit uint32) { + c.glContext.ActiveTexture(gl.Enum(textureUnit)) +} + +func (c *mobileContext) AttachShader(program Program, shader Shader) { + c.glContext.AttachShader(gl.Program(program), gl.Shader(shader)) +} + +func (c *mobileContext) BindBuffer(target uint32, buf Buffer) { + c.glContext.BindBuffer(gl.Enum(target), gl.Buffer(buf)) +} + +func (c *mobileContext) BindTexture(target uint32, texture Texture) { + c.glContext.BindTexture(gl.Enum(target), gl.Texture(texture)) +} + +func (c *mobileContext) BlendColor(r, g, b, a float32) { + c.glContext.BlendColor(r, g, b, a) +} + +func (c *mobileContext) BlendFunc(srcFactor, destFactor uint32) { + c.glContext.BlendFunc(gl.Enum(srcFactor), gl.Enum(destFactor)) +} + +func (c *mobileContext) BufferData(target uint32, points []float32, usage uint32) { + data := toLEByteOrder(points...) + c.glContext.BufferData(gl.Enum(target), data, gl.Enum(usage)) +} + +func (c *mobileContext) BufferSubData(target uint32, points []float32) { + data := toLEByteOrder(points...) + c.glContext.BufferSubData(gl.Enum(target), data) +} + +func (c *mobileContext) Clear(mask uint32) { + c.glContext.Clear(gl.Enum(mask)) +} + +func (c *mobileContext) ClearColor(r, g, b, a float32) { + c.glContext.ClearColor(r, g, b, a) +} + +func (c *mobileContext) CompileShader(shader Shader) { + c.glContext.CompileShader(gl.Shader(shader)) +} + +func (c *mobileContext) CreateBuffer() Buffer { + return Buffer(c.glContext.CreateBuffer()) +} + +func (c *mobileContext) CreateProgram() Program { + return Program(c.glContext.CreateProgram()) +} + +func (c *mobileContext) CreateShader(typ uint32) Shader { + return Shader(c.glContext.CreateShader(gl.Enum(typ))) +} + +func (c *mobileContext) CreateTexture() (texture Texture) { + return Texture(c.glContext.CreateTexture()) +} + +func (c *mobileContext) DeleteBuffer(buffer Buffer) { + c.glContext.DeleteBuffer(gl.Buffer(buffer)) +} + +func (c *mobileContext) DeleteTexture(texture Texture) { + c.glContext.DeleteTexture(gl.Texture(texture)) +} + +func (c *mobileContext) Disable(capability uint32) { + c.glContext.Disable(gl.Enum(capability)) +} + +func (c *mobileContext) DrawArrays(mode uint32, first, count int) { + c.glContext.DrawArrays(gl.Enum(mode), first, count) +} + +func (c *mobileContext) Enable(capability uint32) { + c.glContext.Enable(gl.Enum(capability)) +} + +func (c *mobileContext) EnableVertexAttribArray(attribute Attribute) { + c.glContext.EnableVertexAttribArray(gl.Attrib(attribute)) +} + +func (c *mobileContext) GetAttribLocation(program Program, name string) Attribute { + return Attribute(c.glContext.GetAttribLocation(gl.Program(program), name)) +} + +func (c *mobileContext) GetError() uint32 { + return uint32(c.glContext.GetError()) +} + +func (c *mobileContext) GetProgrami(program Program, param uint32) int { + return c.glContext.GetProgrami(gl.Program(program), gl.Enum(param)) +} + +func (c *mobileContext) GetProgramInfoLog(program Program) string { + return c.glContext.GetProgramInfoLog(gl.Program(program)) +} + +func (c *mobileContext) GetShaderi(shader Shader, param uint32) int { + return c.glContext.GetShaderi(gl.Shader(shader), gl.Enum(param)) +} + +func (c *mobileContext) GetShaderInfoLog(shader Shader) string { + return c.glContext.GetShaderInfoLog(gl.Shader(shader)) +} + +func (c *mobileContext) GetUniformLocation(program Program, name string) Uniform { + return Uniform(c.glContext.GetUniformLocation(gl.Program(program), name)) +} + +func (c *mobileContext) LinkProgram(program Program) { + c.glContext.LinkProgram(gl.Program(program)) +} + +func (c *mobileContext) ReadBuffer(_ uint32) { +} + +func (c *mobileContext) ReadPixels(x, y, width, height int, colorFormat, typ uint32, pixels []uint8) { + c.glContext.ReadPixels(pixels, x, y, width, height, gl.Enum(colorFormat), gl.Enum(typ)) +} + +func (c *mobileContext) Scissor(x, y, w, h int32) { + c.glContext.Scissor(x, y, w, h) +} + +func (c *mobileContext) ShaderSource(shader Shader, source string) { + c.glContext.ShaderSource(gl.Shader(shader), source) +} + +func (c *mobileContext) TexImage2D(target uint32, level, width, height int, colorFormat, typ uint32, data []uint8) { + c.glContext.TexImage2D( + gl.Enum(target), + level, + int(colorFormat), + width, + height, + gl.Enum(colorFormat), + gl.Enum(typ), + data, + ) +} + +func (c *mobileContext) TexParameteri(target, param uint32, value int32) { + c.glContext.TexParameteri(gl.Enum(target), gl.Enum(param), int(value)) +} + +func (c *mobileContext) Uniform1f(uniform Uniform, v float32) { + c.glContext.Uniform1f(gl.Uniform(uniform), v) +} + +func (c *mobileContext) Uniform2f(uniform Uniform, v0, v1 float32) { + c.glContext.Uniform2f(gl.Uniform(uniform), v0, v1) +} + +func (c *mobileContext) Uniform4f(uniform Uniform, v0, v1, v2, v3 float32) { + c.glContext.Uniform4f(gl.Uniform(uniform), v0, v1, v2, v3) +} + +func (c *mobileContext) UseProgram(program Program) { + c.glContext.UseProgram(gl.Program(program)) +} + +func (c *mobileContext) VertexAttribPointerWithOffset(attribute Attribute, size int, typ uint32, normalized bool, stride, offset int) { + c.glContext.VertexAttribPointer(gl.Attrib(attribute), size, gl.Enum(typ), normalized, stride, offset) +} + +func (c *mobileContext) Viewport(x, y, width, height int) { + c.glContext.Viewport(x, y, width, height) +} + +// toLEByteOrder returns the byte representation of float32 values in little endian byte order. +func toLEByteOrder(values ...float32) []byte { + b := make([]byte, 4*len(values)) + for i, v := range values { + u := math.Float32bits(v) + b[4*i+0] = byte(u >> 0) + b[4*i+1] = byte(u >> 8) + b[4*i+2] = byte(u >> 16) + b[4*i+3] = byte(u >> 24) + } + return b +} diff --git a/vendor/fyne.io/fyne/v2/internal/painter/gl/gl_wasm.go b/vendor/fyne.io/fyne/v2/internal/painter/gl/gl_wasm.go new file mode 100644 index 0000000..0ab63bc --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/painter/gl/gl_wasm.go @@ -0,0 +1,343 @@ +//go:build wasm || test_web_driver + +package gl + +import ( + "math" + + "github.com/fyne-io/gl-js" +) + +const ( + arrayBuffer = gl.ARRAY_BUFFER + bitColorBuffer = gl.COLOR_BUFFER_BIT + bitDepthBuffer = gl.DEPTH_BUFFER_BIT + clampToEdge = gl.CLAMP_TO_EDGE + colorFormatRGBA = gl.RGBA + compileStatus = gl.COMPILE_STATUS + constantAlpha = gl.CONSTANT_ALPHA + float = gl.FLOAT + fragmentShader = gl.FRAGMENT_SHADER + front = gl.FRONT + glFalse = gl.FALSE + linkStatus = gl.LINK_STATUS + one = gl.ONE + oneMinusConstantAlpha = gl.ONE_MINUS_CONSTANT_ALPHA + oneMinusSrcAlpha = gl.ONE_MINUS_SRC_ALPHA + scissorTest = gl.SCISSOR_TEST + srcAlpha = gl.SRC_ALPHA + staticDraw = gl.STATIC_DRAW + texture0 = gl.TEXTURE0 + texture2D = gl.TEXTURE_2D + textureMinFilter = gl.TEXTURE_MIN_FILTER + textureMagFilter = gl.TEXTURE_MAG_FILTER + textureWrapS = gl.TEXTURE_WRAP_S + textureWrapT = gl.TEXTURE_WRAP_T + triangles = gl.TRIANGLES + triangleStrip = gl.TRIANGLE_STRIP + unsignedByte = gl.UNSIGNED_BYTE + vertexShader = gl.VERTEX_SHADER +) + +type ( + // Attribute represents a GL attribute + Attribute gl.Attrib + // Buffer represents a GL buffer + Buffer gl.Buffer + // Program represents a compiled GL program + Program gl.Program + // Shader represents a GL shader + Shader gl.Shader + // Uniform represents a GL uniform + Uniform gl.Uniform +) + +var ( + noBuffer = Buffer(gl.NoBuffer) + noShader = Shader(gl.NoShader) + textureFilterToGL = [...]int32{gl.LINEAR, gl.NEAREST} +) + +func (p *painter) Init() { + p.ctx = &xjsContext{} + gl.Disable(gl.DEPTH_TEST) + gl.Enable(gl.BLEND) + p.logError() + p.program = ProgramState{ + ref: p.createProgram("simple_es"), + buff: p.createBuffer(20), + uniforms: make(map[string]*UniformState), + attributes: make(map[string]Attribute), + } + p.getUniformLocations(p.program, "text", "alpha", "cornerRadius", "size", "inset") + p.enableAttribArrays(p.program, "vert", "vertTexCoord") + + p.lineProgram = ProgramState{ + ref: p.createProgram("line_es"), + buff: p.createBuffer(24), + uniforms: make(map[string]*UniformState), + attributes: make(map[string]Attribute), + } + p.getUniformLocations(p.lineProgram, "color", "feather", "lineWidth") + p.enableAttribArrays(p.lineProgram, "vert", "normal") + + p.rectangleProgram = ProgramState{ + ref: p.createProgram("rectangle_es"), + buff: p.createBuffer(16), + uniforms: make(map[string]*UniformState), + attributes: make(map[string]Attribute), + } + p.getUniformLocations( + p.rectangleProgram, + "frame_size", "rect_coords", "stroke_width", "fill_color", "stroke_color", + ) + p.enableAttribArrays(p.rectangleProgram, "vert", "normal") + + p.roundRectangleProgram = ProgramState{ + ref: p.createProgram("round_rectangle_es"), + buff: p.createBuffer(16), + uniforms: make(map[string]*UniformState), + attributes: make(map[string]Attribute), + } + p.getUniformLocations(p.roundRectangleProgram, + "frame_size", "rect_coords", + "stroke_width_half", "rect_size_half", + "radius", "edge_softness", + "fill_color", "stroke_color", + ) + p.enableAttribArrays(p.roundRectangleProgram, "vert", "normal") + + p.polygonProgram = ProgramState{ + ref: p.createProgram("polygon_es"), + buff: p.createBuffer(16), + uniforms: make(map[string]*UniformState), + attributes: make(map[string]Attribute), + } + p.getUniformLocations(p.polygonProgram, + "frame_size", "rect_coords", "edge_softness", + "outer_radius", "angle", "sides", + "fill_color", "corner_radius", + "stroke_width", "stroke_color", + ) + p.enableAttribArrays(p.polygonProgram, "vert", "normal") + + p.arcProgram = ProgramState{ + ref: p.createProgram("arc_es"), + buff: p.createBuffer(16), + uniforms: make(map[string]*UniformState), + attributes: make(map[string]Attribute), + } + p.getUniformLocations(p.arcProgram, + "frame_size", "rect_coords", + "inner_radius", "outer_radius", + "start_angle", "end_angle", + "edge_softness", "corner_radius", + "stroke_width", "stroke_color", + "fill_color", + ) + p.enableAttribArrays(p.arcProgram, "vert", "normal") +} + +func (p *painter) getUniformLocations(pState ProgramState, names ...string) { + for _, name := range names { + u := p.ctx.GetUniformLocation(pState.ref, name) + pState.uniforms[name] = &UniformState{ref: u} + } +} + +func (p *painter) enableAttribArrays(pState ProgramState, names ...string) { + for _, name := range names { + a := p.ctx.GetAttribLocation(pState.ref, name) + p.ctx.EnableVertexAttribArray(a) + pState.attributes[name] = a + } +} + +type xjsContext struct{} + +var _ context = (*xjsContext)(nil) + +func (c *xjsContext) ActiveTexture(textureUnit uint32) { + gl.ActiveTexture(gl.Enum(textureUnit)) +} + +func (c *xjsContext) AttachShader(program Program, shader Shader) { + gl.AttachShader(gl.Program(program), gl.Shader(shader)) +} + +func (c *xjsContext) BindBuffer(target uint32, buf Buffer) { + gl.BindBuffer(gl.Enum(target), gl.Buffer(buf)) +} + +func (c *xjsContext) BindTexture(target uint32, texture Texture) { + gl.BindTexture(gl.Enum(target), gl.Texture(texture)) +} + +func (c *xjsContext) BlendColor(r, g, b, a float32) { + gl.BlendColor(r, g, b, a) +} + +func (c *xjsContext) BlendFunc(srcFactor, destFactor uint32) { + gl.BlendFunc(gl.Enum(srcFactor), gl.Enum(destFactor)) +} + +func (c *xjsContext) BufferData(target uint32, points []float32, usage uint32) { + gl.BufferData(gl.Enum(target), toLEByteOrder(points...), gl.Enum(usage)) +} + +func (c *xjsContext) BufferSubData(target uint32, points []float32) { + data := toLEByteOrder(points...) + gl.BufferSubData(gl.Enum(target), 0, data) +} + +func (c *xjsContext) Clear(mask uint32) { + gl.Clear(gl.Enum(mask)) +} + +func (c *xjsContext) ClearColor(r, g, b, a float32) { + gl.ClearColor(r, g, b, a) +} + +func (c *xjsContext) CompileShader(shader Shader) { + gl.CompileShader(gl.Shader(shader)) +} + +func (c *xjsContext) CreateBuffer() Buffer { + return Buffer(gl.CreateBuffer()) +} + +func (c *xjsContext) CreateProgram() Program { + return Program(gl.CreateProgram()) +} + +func (c *xjsContext) CreateShader(typ uint32) Shader { + return Shader(gl.CreateShader(gl.Enum(typ))) +} + +func (c *xjsContext) CreateTexture() (texture Texture) { + return Texture(gl.CreateTexture()) +} + +func (c *xjsContext) DeleteBuffer(buffer Buffer) { + gl.DeleteBuffer(gl.Buffer(buffer)) +} + +func (c *xjsContext) DeleteTexture(texture Texture) { + gl.DeleteTexture(gl.Texture(texture)) +} + +func (c *xjsContext) Disable(capability uint32) { + gl.Disable(gl.Enum(capability)) +} + +func (c *xjsContext) DrawArrays(mode uint32, first, count int) { + gl.DrawArrays(gl.Enum(mode), first, count) +} + +func (c *xjsContext) Enable(capability uint32) { + gl.Enable(gl.Enum(capability)) +} + +func (c *xjsContext) EnableVertexAttribArray(attribute Attribute) { + gl.EnableVertexAttribArray(gl.Attrib(attribute)) +} + +func (c *xjsContext) GetAttribLocation(program Program, name string) Attribute { + return Attribute(gl.GetAttribLocation(gl.Program(program), name)) +} + +func (c *xjsContext) GetError() uint32 { + return uint32(gl.GetError()) +} + +func (c *xjsContext) GetProgrami(program Program, param uint32) int { + return gl.GetProgrami(gl.Program(program), gl.Enum(param)) +} + +func (c *xjsContext) GetProgramInfoLog(program Program) string { + return gl.GetProgramInfoLog(gl.Program(program)) +} + +func (c *xjsContext) GetShaderi(shader Shader, param uint32) int { + return gl.GetShaderi(gl.Shader(shader), gl.Enum(param)) +} + +func (c *xjsContext) GetShaderInfoLog(shader Shader) string { + return gl.GetShaderInfoLog(gl.Shader(shader)) +} + +func (c *xjsContext) GetUniformLocation(program Program, name string) Uniform { + return Uniform(gl.GetUniformLocation(gl.Program(program), name)) +} + +func (c *xjsContext) LinkProgram(program Program) { + gl.LinkProgram(gl.Program(program)) +} + +func (c *xjsContext) ReadBuffer(_ uint32) { +} + +func (c *xjsContext) ReadPixels(x, y, width, height int, colorFormat, typ uint32, pixels []uint8) { + gl.ReadPixels(pixels, x, y, width, height, gl.Enum(colorFormat), gl.Enum(typ)) +} + +func (c *xjsContext) ShaderSource(shader Shader, source string) { + gl.ShaderSource(gl.Shader(shader), source) +} + +func (c *xjsContext) Scissor(x, y, w, h int32) { + gl.Scissor(x, y, w, h) +} + +func (c *xjsContext) TexImage2D(target uint32, level, width, height int, colorFormat, typ uint32, data []uint8) { + gl.TexImage2D( + gl.Enum(target), + level, + width, + height, + gl.Enum(colorFormat), + gl.Enum(typ), + data, + ) +} + +func (c *xjsContext) TexParameteri(target, param uint32, value int32) { + gl.TexParameteri(gl.Enum(target), gl.Enum(param), int(value)) +} + +func (c *xjsContext) Uniform1f(uniform Uniform, v float32) { + gl.Uniform1f(gl.Uniform(uniform), v) +} + +func (c *xjsContext) Uniform2f(uniform Uniform, v0, v1 float32) { + gl.Uniform2f(gl.Uniform(uniform), v0, v1) +} + +func (c *xjsContext) Uniform4f(uniform Uniform, v0, v1, v2, v3 float32) { + gl.Uniform4f(gl.Uniform(uniform), v0, v1, v2, v3) +} + +func (c *xjsContext) UseProgram(program Program) { + gl.UseProgram(gl.Program(program)) +} + +func (c *xjsContext) VertexAttribPointerWithOffset(attribute Attribute, size int, typ uint32, normalized bool, stride, offset int) { + gl.VertexAttribPointer(gl.Attrib(attribute), size, gl.Enum(typ), normalized, stride, offset) +} + +func (c *xjsContext) Viewport(x, y, width, height int) { + gl.Viewport(x, y, width, height) +} + +// toLEByteOrder returns the byte representation of float32 values in little endian byte order. +func toLEByteOrder(values ...float32) []byte { + b := make([]byte, 4*len(values)) + for i, v := range values { + u := math.Float32bits(v) + b[4*i+0] = byte(u >> 0) + b[4*i+1] = byte(u >> 8) + b[4*i+2] = byte(u >> 16) + b[4*i+3] = byte(u >> 24) + } + return b +} diff --git a/vendor/fyne.io/fyne/v2/internal/painter/gl/painter.go b/vendor/fyne.io/fyne/v2/internal/painter/gl/painter.go new file mode 100644 index 0000000..86aff21 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/painter/gl/painter.go @@ -0,0 +1,219 @@ +// Package gl provides a full Fyne render implementation using system OpenGL libraries. +package gl + +import ( + "fmt" + "image" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/driver" + "fyne.io/fyne/v2/theme" +) + +// Painter defines the functionality of our OpenGL based renderer +type Painter interface { + // Init tell a new painter to initialise, usually called after a context is available + Init() + // Capture requests that the specified canvas be drawn to an in-memory image + Capture(fyne.Canvas) image.Image + // Clear tells our painter to prepare a fresh paint + Clear() + // Free is used to indicate that a certain canvas object is no longer needed + Free(fyne.CanvasObject) + // Paint a single fyne.CanvasObject but not its children. + Paint(fyne.CanvasObject, fyne.Position, fyne.Size) + // SetFrameBufferScale tells us when we have more than 1 framebuffer pixel for each output pixel + SetFrameBufferScale(float32) + // SetOutputSize is used to change the resolution of our output viewport + SetOutputSize(int, int) + // StartClipping tells us that the following paint actions should be clipped to the specified area. + StartClipping(fyne.Position, fyne.Size) + // StopClipping stops clipping paint actions. + StopClipping() +} + +// NewPainter creates a new GL based renderer for the provided canvas. +// If it is a master painter it will also initialise OpenGL +func NewPainter(c fyne.Canvas, ctx driver.WithContext) Painter { + p := &painter{canvas: c, contextProvider: ctx} + p.SetFrameBufferScale(1.0) + return p +} + +type painter struct { + canvas fyne.Canvas + ctx context + contextProvider driver.WithContext + program ProgramState + lineProgram ProgramState + rectangleProgram ProgramState + roundRectangleProgram ProgramState + polygonProgram ProgramState + arcProgram ProgramState + texScale float32 + pixScale float32 // pre-calculate scale*texScale for each draw +} + +type ProgramState struct { + ref Program + buff Buffer + uniforms map[string]*UniformState + attributes map[string]Attribute +} + +type UniformState struct { + ref Uniform + prev [4]float32 +} + +func (p *painter) SetUniform1f(pState ProgramState, name string, v float32) { + u := pState.uniforms[name] + if u.prev[0] == v { + return + } + u.prev[0] = v + p.ctx.Uniform1f(u.ref, v) +} + +func (p *painter) SetUniform2f(pState ProgramState, name string, v0, v1 float32) { + u := pState.uniforms[name] + if u.prev[0] == v0 && u.prev[1] == v1 { + return + } + u.prev[0] = v0 + u.prev[1] = v1 + p.ctx.Uniform2f(u.ref, v0, v1) +} + +func (p *painter) SetUniform4f(pState ProgramState, name string, v0, v1, v2, v3 float32) { + u := pState.uniforms[name] + if u.prev[0] == v0 && u.prev[1] == v1 && u.prev[2] == v2 && u.prev[3] == v3 { + return + } + u.prev[0] = v0 + u.prev[1] = v1 + u.prev[2] = v2 + u.prev[3] = v3 + p.ctx.Uniform4f(u.ref, v0, v1, v2, v3) +} + +func (p *painter) UpdateVertexArray(pState ProgramState, name string, size, stride, offset int) { + a := pState.attributes[name] + + p.ctx.VertexAttribPointerWithOffset(a, size, float, false, stride*floatSize, offset*floatSize) + p.logError() +} + +// Declare conformity to Painter interface +var _ Painter = (*painter)(nil) + +func (p *painter) Clear() { + r, g, b, a := theme.Color(theme.ColorNameBackground).RGBA() + p.ctx.ClearColor(float32(r)/max16bit, float32(g)/max16bit, float32(b)/max16bit, float32(a)/max16bit) + p.ctx.Clear(bitColorBuffer | bitDepthBuffer) + p.logError() +} + +func (p *painter) Free(obj fyne.CanvasObject) { + p.freeTexture(obj) +} + +func (p *painter) Paint(obj fyne.CanvasObject, pos fyne.Position, frame fyne.Size) { + if obj.Visible() { + p.drawObject(obj, pos, frame) + } +} + +func (p *painter) SetFrameBufferScale(scale float32) { + p.texScale = scale + p.pixScale = p.canvas.Scale() * p.texScale +} + +func (p *painter) SetOutputSize(width, height int) { + p.ctx.Viewport(0, 0, width, height) + p.logError() +} + +func (p *painter) StartClipping(pos fyne.Position, size fyne.Size) { + x := p.textureScale(pos.X) + y := p.textureScale(p.canvas.Size().Height - pos.Y - size.Height) + w := p.textureScale(size.Width) + h := p.textureScale(size.Height) + p.ctx.Scissor(int32(x), int32(y), int32(w), int32(h)) + p.ctx.Enable(scissorTest) + p.logError() +} + +func (p *painter) StopClipping() { + p.ctx.Disable(scissorTest) + p.logError() +} + +func (p *painter) compileShader(source string, shaderType uint32) (Shader, error) { + shader := p.ctx.CreateShader(shaderType) + + p.ctx.ShaderSource(shader, source) + p.logError() + p.ctx.CompileShader(shader) + p.logError() + + info := p.ctx.GetShaderInfoLog(shader) + if p.ctx.GetShaderi(shader, compileStatus) == glFalse { + return noShader, fmt.Errorf("failed to compile OpenGL shader:\n%s\n>>> SHADER SOURCE\n%s\n<<< SHADER SOURCE", info, source) + } + + // The info is probably a null terminated string. + // An empty info has been seen as "\x00" or "\x00\x00". + if len(info) > 0 && info != "\x00" && info != "\x00\x00" { + fmt.Printf("OpenGL shader compilation output:\n%s\n>>> SHADER SOURCE\n%s\n<<< SHADER SOURCE\n", info, source) + } + + return shader, nil +} + +func (p *painter) createProgram(shaderFilename string) Program { + // Why a switch over a filename? + // Because this allows for a minimal change, once we reach Go 1.16 and use go:embed instead of + // fyne bundle. + vertexSrc, fragmentSrc := shaderSourceNamed(shaderFilename) + if vertexSrc == nil { + panic("shader not found: " + shaderFilename) + } + + vertShader, err := p.compileShader(string(vertexSrc), vertexShader) + if err != nil { + panic(err) + } + fragShader, err := p.compileShader(string(fragmentSrc), fragmentShader) + if err != nil { + panic(err) + } + + prog := p.ctx.CreateProgram() + p.ctx.AttachShader(prog, vertShader) + p.ctx.AttachShader(prog, fragShader) + p.ctx.LinkProgram(prog) + + info := p.ctx.GetProgramInfoLog(prog) + if p.ctx.GetProgrami(prog, linkStatus) == glFalse { + panic(fmt.Errorf("failed to link OpenGL program:\n%s", info)) + } + + // The info is probably a null terminated string. + // An empty info has been seen as "\x00" or "\x00\x00". + if len(info) > 0 && info != "\x00" && info != "\x00\x00" { + fmt.Printf("OpenGL program linking output:\n%s\n", info) + } + + if glErr := p.ctx.GetError(); glErr != 0 { + panic(fmt.Sprintf("failed to link OpenGL program; error code: %x", glErr)) + } + + p.ctx.UseProgram(prog) + + return prog +} + +func (p *painter) logError() { + logGLError(p.ctx.GetError) +} diff --git a/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders.go b/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders.go new file mode 100644 index 0000000..3515da4 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders.go @@ -0,0 +1,52 @@ +//go:build (!gles && !arm && !arm64 && !android && !ios && !mobile && !test_web_driver && !wasm) || (darwin && !mobile && !ios && !wasm && !test_web_driver) + +package gl + +import _ "embed" + +var ( + //go:embed shaders/line.frag + shaderLineFrag []byte + + //go:embed shaders/line.vert + shaderLineVert []byte + + //go:embed shaders/rectangle.frag + shaderRectangleFrag []byte + + //go:embed shaders/rectangle.vert + shaderRectangleVert []byte + + //go:embed shaders/round_rectangle.frag + shaderRoundrectangleFrag []byte + + //go:embed shaders/simple.frag + shaderSimpleFrag []byte + + //go:embed shaders/simple.vert + shaderSimpleVert []byte + + //go:embed shaders/polygon.frag + shaderPolygonFrag []byte + + //go:embed shaders/arc.frag + shaderArcFrag []byte +) + +func shaderSourceNamed(name string) ([]byte, []byte) { + switch name { + case "line": + return shaderLineVert, shaderLineFrag + case "simple": + return shaderSimpleVert, shaderSimpleFrag + case "rectangle": + return shaderRectangleVert, shaderRectangleFrag + case "round_rectangle": + return shaderRectangleVert, shaderRoundrectangleFrag + case "polygon": + return shaderRectangleVert, shaderPolygonFrag + case "arc": + return shaderRectangleVert, shaderArcFrag + } + return nil, nil +} diff --git a/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/arc.frag b/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/arc.frag new file mode 100644 index 0000000..c45f88e --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/arc.frag @@ -0,0 +1,116 @@ +#version 110 + +// Note: This shader operates in the unit circle coordinate system, where angles are measured from the positive X axis. +// To adapt the arc orientation or coordinate system, adjust the start_angle and end_angle uniforms accordingly. + +uniform vec2 frame_size; +uniform vec4 rect_coords; +uniform float edge_softness; + +uniform float inner_radius; +uniform float outer_radius; +uniform float start_angle; +uniform float end_angle; +uniform vec4 fill_color; +uniform float corner_radius; +uniform float stroke_width; +uniform vec4 stroke_color; + +const float PI = 3.141592653589793; + +// Computes the signed distance for a rounded arc shape +// Parameters: +// position - The 2D coordinate to evaluate (vec2) +// inner radius - The inner radius of the arc (float) +// outer radius - The outer radius of the arc (float) +// start rad - The starting angle of the arc in radians (float) +// end rad - The ending angle of the arc in radians (float) +// corner radius - The radius for rounding the arc's corners (float) +// Returns: +// The signed distance from the given position to the edge of the rounded arc +// Negative values are inside the arc, positive values are outside, and zero is on the edge +float sd_rounded_arc(vec2 p, float r1, float r2, float a0, float a1, float cr) +{ + // center the arc for simpler calculations + float mid_angle = (a0 + a1) / 2.0; + float arc_span = abs(a1 - a0); + + float cs = cos(mid_angle); + float sn = sin(mid_angle); + p = mat2(cs, -sn, sn, cs) * p; + + // calculate distance to a rounded box in a pseudo-polar space + float r = length(p); + + // atan(y, x) for standard angle convention (0 degrees = right) + float a = atan(p.y, p.x); + + vec2 box_half_size = vec2(arc_span * 0.5 * r, (r2 - r1) * 0.5); + vec2 q = vec2(a * r, r - (r1 + r2) * 0.5); + + // the inner corner radius is clamped to half of the smaller dimension: + // thickness (r2 - r1), to prevent inner/outer corners on the same side from overlapping + // inner length (arc_span * r1), to prevent the start/end inner corners from overlapping + float inner_cr = min(cr, 0.5 * min(r2 - r1, arc_span * r1)); + // the outer corner radius is just cr + float outer_cr = cr; + + // interpolate between inner and outer corner radius based on the radial position + // 't' goes from 0 (inner) to 1 (outer). + float t = smoothstep(-box_half_size.y, box_half_size.y, q.y); + float effective_cr = mix(inner_cr, outer_cr, t); + + // use the standard SDF for a 2D rounded box with the effective radius + vec2 dist = abs(q) - box_half_size + effective_cr; + return length(max(dist, 0.0)) + min(max(dist.x, dist.y), 0.0) - effective_cr; +} + +void main() +{ + vec4 frag_rect_coords = vec4(rect_coords[0], rect_coords[1], frame_size.y - rect_coords[3], frame_size.y - rect_coords[2]); + vec2 vec_centered_pos = (gl_FragCoord.xy - vec2(frag_rect_coords[0] + frag_rect_coords[1], frag_rect_coords[2] + frag_rect_coords[3]) * 0.5); + float start_rad = radians(start_angle); + float end_rad = radians(end_angle); + + // check if the arc is a full circle (360 degrees or more) + // the sd_rounded_arc function creates segment at the start/end angle, which is undesirable for a complete circle + float dist; + if (abs(end_rad - start_rad) >= 2.0 * PI - 0.001) + { + // full circle + float r = length(vec_centered_pos); + + if (inner_radius < 0.5) + { + // no inner radius + dist = r - outer_radius; + } + else + { + float ring_center_radius = (inner_radius + outer_radius) * 0.5; + float ring_thickness = (outer_radius - inner_radius) * 0.5; + dist = abs(r - ring_center_radius) - ring_thickness; + } + } + else + { + dist = sd_rounded_arc(vec_centered_pos, inner_radius, outer_radius, start_rad, end_rad, corner_radius); + } + + vec4 final_color = fill_color; + + if (stroke_width > 0.0) + { + // create a mask for the fill area (inside, shrunk by stroke width) + float fill_mask = smoothstep(edge_softness, -edge_softness, dist + stroke_width); + + // combine fill mask and colors (fill + stroke) + final_color = mix(stroke_color, fill_color, fill_mask); + } + + // smooth edges + float final_alpha = smoothstep(edge_softness, -edge_softness, dist); + + // apply the final alpha to the combined color + gl_FragColor = vec4(final_color.rgb, final_color.a * final_alpha); +} diff --git a/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/arc_es.frag b/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/arc_es.frag new file mode 100644 index 0000000..5a0417c --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/arc_es.frag @@ -0,0 +1,126 @@ +#version 100 + +// Note: This shader operates in the unit circle coordinate system, where angles are measured from the positive X axis. +// To adapt the arc orientation or coordinate system, adjust the start_angle and end_angle uniforms accordingly. + +#ifdef GL_ES +# ifdef GL_FRAGMENT_PRECISION_HIGH +precision highp float; +# else +precision mediump float; +#endif +precision mediump int; +precision lowp sampler2D; +#endif + +uniform vec2 frame_size; +uniform vec4 rect_coords; +uniform float edge_softness; + +uniform float inner_radius; +uniform float outer_radius; +uniform float start_angle; +uniform float end_angle; +uniform vec4 fill_color; +uniform float corner_radius; +uniform float stroke_width; +uniform vec4 stroke_color; + +const float PI = 3.141592653589793; + +// Computes the signed distance for a rounded arc shape +// Parameters: +// position - The 2D coordinate to evaluate (vec2) +// inner radius - The inner radius of the arc (float) +// outer radius - The outer radius of the arc (float) +// start rad - The starting angle of the arc in radians (float) +// end rad - The ending angle of the arc in radians (float) +// corner radius - The radius for rounding the arc's corners (float) +// Returns: +// The signed distance from the given position to the edge of the rounded arc +// Negative values are inside the arc, positive values are outside, and zero is on the edge +float sd_rounded_arc(vec2 p, float r1, float r2, float a0, float a1, float cr) +{ + // center the arc for simpler calculations + float mid_angle = (a0 + a1) / 2.0; + float arc_span = abs(a1 - a0); + + float cs = cos(mid_angle); + float sn = sin(mid_angle); + p = mat2(cs, -sn, sn, cs) * p; + + // calculate distance to a rounded box in a pseudo-polar space + float r = length(p); + + // atan(y, x) for standard angle convention (0 degrees = right) + float a = atan(p.y, p.x); + + vec2 box_half_size = vec2(arc_span * 0.5 * r, (r2 - r1) * 0.5); + vec2 q = vec2(a * r, r - (r1 + r2) * 0.5); + + // the inner corner radius is clamped to half of the smaller dimension: + // thickness (r2 - r1), to prevent inner/outer corners on the same side from overlapping + // inner length (arc_span * r1), to prevent the start/end inner corners from overlapping + float inner_cr = min(cr, 0.5 * min(r2 - r1, arc_span * r1)); + // the outer corner radius is just cr + float outer_cr = cr; + + // interpolate between inner and outer corner radius based on the radial position + // 't' goes from 0 (inner) to 1 (outer). + float t = smoothstep(-box_half_size.y, box_half_size.y, q.y); + float effective_cr = mix(inner_cr, outer_cr, t); + + // use the standard SDF for a 2D rounded box with the effective radius + vec2 dist = abs(q) - box_half_size + effective_cr; + return length(max(dist, 0.0)) + min(max(dist.x, dist.y), 0.0) - effective_cr; +} + +void main() +{ + vec4 frag_rect_coords = vec4(rect_coords[0], rect_coords[1], frame_size.y - rect_coords[3], frame_size.y - rect_coords[2]); + vec2 vec_centered_pos = (gl_FragCoord.xy - vec2(frag_rect_coords[0] + frag_rect_coords[1], frag_rect_coords[2] + frag_rect_coords[3]) * 0.5); + float start_rad = radians(start_angle); + float end_rad = radians(end_angle); + + // check if the arc is a full circle (360 degrees or more) + // the sd_rounded_arc function creates segment at the start/end angle, which is undesirable for a complete circle + float dist; + if (abs(end_rad - start_rad) >= 2.0 * PI - 0.001) + { + // full circle + float r = length(vec_centered_pos); + + if (inner_radius < 0.5) + { + // no inner radius + dist = r - outer_radius; + } + else + { + float ring_center_radius = (inner_radius + outer_radius) * 0.5; + float ring_thickness = (outer_radius - inner_radius) * 0.5; + dist = abs(r - ring_center_radius) - ring_thickness; + } + } + else + { + dist = sd_rounded_arc(vec_centered_pos, inner_radius, outer_radius, start_rad, end_rad, corner_radius); + } + + vec4 final_color = fill_color; + + if (stroke_width > 0.0) + { + // create a mask for the fill area (inside, shrunk by stroke width) + float fill_mask = smoothstep(edge_softness, -edge_softness, dist + stroke_width); + + // combine fill mask and colors (fill + stroke) + final_color = mix(stroke_color, fill_color, fill_mask); + } + + // smooth edges + float final_alpha = smoothstep(edge_softness, -edge_softness, dist); + + // apply the final alpha to the combined color + gl_FragColor = vec4(final_color.rgb, final_color.a * final_alpha); +} diff --git a/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/line.frag b/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/line.frag new file mode 100644 index 0000000..266f159 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/line.frag @@ -0,0 +1,18 @@ +#version 110 + +uniform vec4 color; +uniform float lineWidth; +uniform float feather; + +varying vec2 delta; + +void main() { + float alpha = color.a; + float distance = length(delta); + + if (feather == 0.0 || distance <= lineWidth - feather) { + gl_FragColor = color; + } else { + gl_FragColor = vec4(color.r, color.g, color.b, mix(color.a, 0.0, (distance - (lineWidth - feather)) / feather)); + } +} diff --git a/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/line.vert b/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/line.vert new file mode 100644 index 0000000..a243b07 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/line.vert @@ -0,0 +1,14 @@ +#version 110 + +attribute vec2 vert; +attribute vec2 normal; + +uniform float lineWidth; + +varying vec2 delta; + +void main() { + delta = normal * lineWidth; + + gl_Position = vec4(vert + delta, 0, 1); +} diff --git a/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/line_es.frag b/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/line_es.frag new file mode 100644 index 0000000..577748a --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/line_es.frag @@ -0,0 +1,28 @@ +#version 100 + +#ifdef GL_ES +# ifdef GL_FRAGMENT_PRECISION_HIGH +precision highp float; +# else +precision mediump float; +#endif +precision mediump int; +precision lowp sampler2D; +#endif + +uniform vec4 color; +uniform float lineWidth; +uniform float feather; + +varying vec2 delta; + +void main() { + float alpha = color.a; + float distance = length(delta); + + if (feather == 0.0 || distance <= lineWidth - feather) { + gl_FragColor = color; + } else { + gl_FragColor = vec4(color.r, color.g, color.b, mix(color.a, 0.0, (distance - (lineWidth - feather)) / feather)); + } +} diff --git a/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/line_es.vert b/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/line_es.vert new file mode 100644 index 0000000..1fd10ea --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/line_es.vert @@ -0,0 +1,24 @@ +#version 100 + +#ifdef GL_ES +# ifdef GL_FRAGMENT_PRECISION_HIGH +precision highp float; +# else +precision mediump float; +#endif +precision mediump int; +precision lowp sampler2D; +#endif + +attribute vec2 vert; +attribute vec2 normal; + +uniform float lineWidth; + +varying vec2 delta; + +void main() { + delta = normal * lineWidth; + + gl_Position = vec4(vert + delta, 0, 1); +} diff --git a/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/polygon.frag b/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/polygon.frag new file mode 100644 index 0000000..97f5484 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/polygon.frag @@ -0,0 +1,59 @@ +#version 110 + +uniform vec2 frame_size; +uniform vec4 rect_coords; +uniform float edge_softness; + +uniform float outer_radius; +uniform float angle; +uniform float sides; + +uniform vec4 fill_color; +uniform float corner_radius; +uniform float stroke_width; +uniform vec4 stroke_color; + +const float PI = 3.141592653589793; + +mat2 rotate(float angle) { + float s = sin(-angle); + float c = cos(-angle); + return mat2(c, -s, s, c); +} + +// The signed distance (float) from the point to the regular polygon's edge +float regular_distance(vec2 p, float r, int s) +{ + float angle = PI / float(s); + float angle_cos = cos(angle); + float angle_sin = sin(angle); + float angular_offset = mod(atan(p.x, p.y), 2.0*angle) - angle; + vec2 distance = length(p) * vec2(cos(angular_offset), abs(sin(angular_offset))) - r*vec2(angle_cos, angle_sin); + distance.y += clamp(-distance.y, 0.0, r*angle_sin); + return length(distance) * sign(distance.x); +} + +void main() +{ + vec4 frag_rect_coords = vec4(rect_coords[0], rect_coords[1], frame_size.y - rect_coords[3], frame_size.y - rect_coords[2]); + vec2 vec_centered_pos = (gl_FragCoord.xy - vec2(frag_rect_coords[0] + frag_rect_coords[1], frag_rect_coords[2] + frag_rect_coords[3]) * 0.5); + + vec_centered_pos = rotate(radians(angle)) * vec_centered_pos; + float dist = regular_distance(vec_centered_pos, outer_radius - corner_radius, int(sides)) - corner_radius; + vec4 final_color = fill_color; + + if (stroke_width > 0.0) + { + // create a mask for the fill area (inside, shrunk by stroke width) + float fill_mask = smoothstep(-stroke_width + edge_softness, -stroke_width - edge_softness, dist); + + // combine fill mask and colors (fill + stroke) + final_color = mix(stroke_color, fill_color, fill_mask); + } + + // smooth edges + float final_alpha = smoothstep(edge_softness, -edge_softness, dist); + + // apply the final alpha to the combined color + gl_FragColor = vec4(final_color.rgb, final_color.a * final_alpha); +} diff --git a/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/polygon_es.frag b/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/polygon_es.frag new file mode 100644 index 0000000..3a935ab --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/polygon_es.frag @@ -0,0 +1,69 @@ +#version 100 + +#ifdef GL_ES +# ifdef GL_FRAGMENT_PRECISION_HIGH +precision highp float; +# else +precision mediump float; +#endif +precision mediump int; +precision lowp sampler2D; +#endif + +uniform vec2 frame_size; +uniform vec4 rect_coords; +uniform float edge_softness; + +uniform float outer_radius; +uniform float angle; +uniform float sides; + +uniform vec4 fill_color; +uniform float corner_radius; +uniform float stroke_width; +uniform vec4 stroke_color; + +const float PI = 3.141592653589793; + +mat2 rotate(float angle) { + float s = sin(-angle); + float c = cos(-angle); + return mat2(c, -s, s, c); +} + +// The signed distance (float) from the point to the regular polygon's edge +float regular_distance(vec2 p, float r, int s) +{ + float angle = PI / float(s); + float angle_cos = cos(angle); + float angle_sin = sin(angle); + float angular_offset = mod(atan(p.x, p.y), 2.0*angle) - angle; + vec2 distance = length(p) * vec2(cos(angular_offset), abs(sin(angular_offset))) - r*vec2(angle_cos, angle_sin); + distance.y += clamp(-distance.y, 0.0, r*angle_sin); + return length(distance) * sign(distance.x); +} + +void main() +{ + vec4 frag_rect_coords = vec4(rect_coords[0], rect_coords[1], frame_size.y - rect_coords[3], frame_size.y - rect_coords[2]); + vec2 vec_centered_pos = (gl_FragCoord.xy - vec2(frag_rect_coords[0] + frag_rect_coords[1], frag_rect_coords[2] + frag_rect_coords[3]) * 0.5); + + vec_centered_pos = rotate(radians(angle)) * vec_centered_pos; + float dist = regular_distance(vec_centered_pos, outer_radius - corner_radius, int(sides)) - corner_radius; + vec4 final_color = fill_color; + + if (stroke_width > 0.0) + { + // create a mask for the fill area (inside, shrunk by stroke width) + float fill_mask = smoothstep(-stroke_width + edge_softness, -stroke_width - edge_softness, dist); + + // combine fill mask and colors (fill + stroke) + final_color = mix(stroke_color, fill_color, fill_mask); + } + + // smooth edges + float final_alpha = smoothstep(edge_softness, -edge_softness, dist); + + // apply the final alpha to the combined color + gl_FragColor = vec4(final_color.rgb, final_color.a * final_alpha); +} diff --git a/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/rectangle.frag b/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/rectangle.frag new file mode 100644 index 0000000..b8c7e28 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/rectangle.frag @@ -0,0 +1,32 @@ +#version 110 + +/* scaled params */ +uniform vec2 frame_size; +uniform vec4 rect_coords; //x1 [0], x2 [1], y1 [2], y2 [3]; coords of the rect_frame +uniform float stroke_width; +/* colors params*/ +uniform vec4 fill_color; +uniform vec4 stroke_color; + + +void main() { + + // discard if outside rectangle coords, necessary to draw thin stroke and mitigate inconsistent borders issue + if (gl_FragCoord.x < rect_coords[0] || gl_FragCoord.x > rect_coords[1] || gl_FragCoord.y < frame_size.y - rect_coords[3] || gl_FragCoord.y > frame_size.y - rect_coords[2]) { + discard; + } + + vec4 color = fill_color; + + if (gl_FragCoord.x >= rect_coords[1] - stroke_width ){ + color = stroke_color; + } else if (gl_FragCoord.x <= rect_coords[0] + stroke_width){ + color = stroke_color; + } else if (gl_FragCoord.y <= frame_size.y - rect_coords[3] + stroke_width ){ + color = stroke_color; + } else if (gl_FragCoord.y >= frame_size.y - rect_coords[2] - stroke_width ){ + color = stroke_color; + } + + gl_FragColor = color; +} diff --git a/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/rectangle.vert b/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/rectangle.vert new file mode 100644 index 0000000..d2aae9b --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/rectangle.vert @@ -0,0 +1,8 @@ +#version 110 + +attribute vec2 vert; +attribute vec2 normal; + +void main() { + gl_Position = vec4(vert+normal, 0, 1); +} diff --git a/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/rectangle_es.frag b/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/rectangle_es.frag new file mode 100644 index 0000000..12d77a3 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/rectangle_es.frag @@ -0,0 +1,42 @@ +#version 100 + +#ifdef GL_ES +# ifdef GL_FRAGMENT_PRECISION_HIGH +precision highp float; +# else +precision mediump float; +#endif +precision mediump int; +precision lowp sampler2D; +#endif + +/* scaled params */ +uniform vec2 frame_size; +uniform vec4 rect_coords; //x1 [0], x2 [1], y1 [2], y2 [3]; coords of the rect_frame +uniform float stroke_width; +/* colors params*/ +uniform vec4 fill_color; +uniform vec4 stroke_color; + + +void main() { + + // discard if outside rectangle coords, necessary to draw thin stroke and mitigate inconsistent borders issue + if (gl_FragCoord.x < rect_coords[0] || gl_FragCoord.x > rect_coords[1] || gl_FragCoord.y < frame_size.y - rect_coords[3] || gl_FragCoord.y > frame_size.y - rect_coords[2]) { + discard; + } + + vec4 color = fill_color; + + if (gl_FragCoord.x >= rect_coords[1] - stroke_width ){ + color = stroke_color; + } else if (gl_FragCoord.x <= rect_coords[0] + stroke_width){ + color = stroke_color; + } else if (gl_FragCoord.y <= frame_size.y - rect_coords[3] + stroke_width ){ + color = stroke_color; + } else if (gl_FragCoord.y >= frame_size.y - rect_coords[2] - stroke_width ){ + color = stroke_color; + } + + gl_FragColor = color; +} diff --git a/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/rectangle_es.vert b/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/rectangle_es.vert new file mode 100644 index 0000000..c46159d --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/rectangle_es.vert @@ -0,0 +1,18 @@ +#version 100 + +#ifdef GL_ES +# ifdef GL_FRAGMENT_PRECISION_HIGH +precision highp float; +# else +precision mediump float; +#endif +precision mediump int; +precision lowp sampler2D; +#endif + +attribute vec2 vert; +attribute vec2 normal; + +void main() { + gl_Position = vec4(vert+normal, 0, 1); +} diff --git a/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/round_rectangle.frag b/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/round_rectangle.frag new file mode 100644 index 0000000..b6a15eb --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/round_rectangle.frag @@ -0,0 +1,86 @@ +#version 110 + +/* scaled params */ +uniform vec2 frame_size; +uniform vec4 rect_coords; //x1 [0], x2 [1], y1 [2], y2 [3]; coords of the rect_frame +uniform float stroke_width_half; +uniform vec2 rect_size_half; +uniform vec4 radius; +uniform float edge_softness; +/* colors params*/ +uniform vec4 fill_color; +uniform vec4 stroke_color; + +// distance is calculated for a single quadrant +// returns invalid output if corner radius exceed half of the shorter edge +float calc_distance(vec2 p, vec2 b, vec4 r) +{ + r.xy = (p.x > 0.0) ? r.xy : r.zw; + r.x = (p.y > 0.0) ? r.x : r.y; + + vec2 d = abs(p) - b + r.x; + return min(max(d.x, d.y), 0.0) + length(max(d, 0.0)) - r.x; +} + +// distance is calculated for all necessary quadrants +// corner radius may exceed half of the shorter edge +float calc_distance_all_quadrants(vec2 p, vec2 size, vec4 radius) { + vec2 d = abs(p) - size; + float dist = length(max(d, 0.0)) + min(max(d.x, d.y), 0.0); + + // top-left corner + vec2 p_tl = p - vec2(radius.z - size.x, size.y - radius.z); + if (p_tl.x < 0.0 && p_tl.y > 0.0) dist = max(dist, length(p_tl) - radius.z); + + // top-right corner + vec2 p_tr = p - vec2(size.x - radius.x, size.y - radius.x); + if (p_tr.x > 0.0 && p_tr.y > 0.0) dist = max(dist, length(p_tr) - radius.x); + + // bottom-right corner + vec2 p_br = p - vec2(size.x - radius.y, radius.y - size.y); + if (p_br.x > 0.0 && p_br.y < 0.0) dist = max(dist, length(p_br) - radius.y); + + // bottom-left corner + vec2 p_bl = p - vec2(radius.w - size.x, radius.w - size.y); + if (p_bl.x < 0.0 && p_bl.y < 0.0) dist = max(dist, length(p_bl) - radius.w); + + return dist; +} + +void main() { + vec4 frag_rect_coords = vec4(rect_coords[0], rect_coords[1], frame_size.y - rect_coords[3], frame_size.y - rect_coords[2]); + vec2 vec_centered_pos = (gl_FragCoord.xy - vec2(frag_rect_coords[0] + frag_rect_coords[1], frag_rect_coords[2] + frag_rect_coords[3]) * 0.5); + + float distance; + float max_radius = max(max(radius.x, radius.y), max(radius.z, radius.w)); + vec4 final_color = fill_color; + float final_alpha; + + // subtract a small threshold value to avoid calling calc_distance_all_quadrants when the largest corner radius is very close to half the length of the rectangle's shortest edge + if (max_radius - 0.9 > min(rect_size_half.x, rect_size_half.y) + stroke_width_half) + { + // at least one corner radius is larger than half of the shorter edge + distance = calc_distance_all_quadrants(vec_centered_pos, rect_size_half + stroke_width_half, radius); + final_alpha = 1.0 - smoothstep(-edge_softness, edge_softness, distance); + + if (stroke_width_half > 0.0) + { + float color_blend = 1.0 - smoothstep(stroke_width_half * 2.0 - edge_softness, stroke_width_half * 2.0 + edge_softness, abs(distance)); + final_color = mix(fill_color, stroke_color, color_blend); + } + } + else + { + distance = calc_distance(vec_centered_pos, rect_size_half, radius - stroke_width_half); + final_alpha = 1.0 - smoothstep(stroke_width_half - edge_softness, stroke_width_half + edge_softness, distance); + + if (stroke_width_half > 0.0) + { + float color_blend = smoothstep(-stroke_width_half - edge_softness, -stroke_width_half + edge_softness, distance); + final_color = mix(fill_color, stroke_color, color_blend); + } + } + + // final color + gl_FragColor = vec4(final_color.rgb, final_color.a * final_alpha); +} diff --git a/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/round_rectangle_es.frag b/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/round_rectangle_es.frag new file mode 100644 index 0000000..64e0f0a --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/round_rectangle_es.frag @@ -0,0 +1,96 @@ +#version 100 + +#ifdef GL_ES +# ifdef GL_FRAGMENT_PRECISION_HIGH +precision highp float; +# else +precision mediump float; +#endif +precision mediump int; +precision lowp sampler2D; +#endif + +/* scaled params */ +uniform vec2 frame_size; +uniform vec4 rect_coords; //x1 [0], x2 [1], y1 [2], y2 [3]; coords of the rect_frame +uniform float stroke_width_half; +uniform vec2 rect_size_half; +uniform vec4 radius; +uniform float edge_softness; +/* colors params*/ +uniform vec4 fill_color; +uniform vec4 stroke_color; + +// distance is calculated for a single quadrant +// returns invalid output if corner radius exceed half of the shorter edge +float calc_distance(vec2 p, vec2 b, vec4 r) +{ + r.xy = (p.x > 0.0) ? r.xy : r.zw; + r.x = (p.y > 0.0) ? r.x : r.y; + + vec2 d = abs(p) - b + r.x; + return min(max(d.x, d.y), 0.0) + length(max(d, 0.0)) - r.x; +} + +// distance is calculated for all necessary quadrants +// corner radius may exceed half of the shorter edge +float calc_distance_all_quadrants(vec2 p, vec2 size, vec4 radius) { + vec2 d = abs(p) - size; + float dist = length(max(d, 0.0)) + min(max(d.x, d.y), 0.0); + + // top-left corner + vec2 p_tl = p - vec2(radius.z - size.x, size.y - radius.z); + if (p_tl.x < 0.0 && p_tl.y > 0.0) dist = max(dist, length(p_tl) - radius.z); + + // top-right corner + vec2 p_tr = p - vec2(size.x - radius.x, size.y - radius.x); + if (p_tr.x > 0.0 && p_tr.y > 0.0) dist = max(dist, length(p_tr) - radius.x); + + // bottom-right corner + vec2 p_br = p - vec2(size.x - radius.y, radius.y - size.y); + if (p_br.x > 0.0 && p_br.y < 0.0) dist = max(dist, length(p_br) - radius.y); + + // bottom-left corner + vec2 p_bl = p - vec2(radius.w - size.x, radius.w - size.y); + if (p_bl.x < 0.0 && p_bl.y < 0.0) dist = max(dist, length(p_bl) - radius.w); + + return dist; +} + +void main() { + vec4 frag_rect_coords = vec4(rect_coords[0], rect_coords[1], frame_size.y - rect_coords[3], frame_size.y - rect_coords[2]); + vec2 vec_centered_pos = (gl_FragCoord.xy - vec2(frag_rect_coords[0] + frag_rect_coords[1], frag_rect_coords[2] + frag_rect_coords[3]) * 0.5); + + float distance; + float max_radius = max(max(radius.x, radius.y), max(radius.z, radius.w)); + vec4 final_color = fill_color; + float final_alpha; + + // subtract a small threshold value to avoid calling calc_distance_all_quadrants when the largest corner radius is very close to half the length of the rectangle's shortest edge + if (max_radius - 0.9 > min(rect_size_half.x, rect_size_half.y) + stroke_width_half) + { + // at least one corner radius is larger than half of the shorter edge + distance = calc_distance_all_quadrants(vec_centered_pos, rect_size_half + stroke_width_half, radius); + final_alpha = 1.0 - smoothstep(-edge_softness, edge_softness, distance); + + if (stroke_width_half > 0.0) + { + float color_blend = 1.0 - smoothstep(stroke_width_half * 2.0 - edge_softness, stroke_width_half * 2.0 + edge_softness, abs(distance)); + final_color = mix(fill_color, stroke_color, color_blend); + } + } + else + { + distance = calc_distance(vec_centered_pos, rect_size_half, radius - stroke_width_half); + final_alpha = 1.0 - smoothstep(stroke_width_half - edge_softness, stroke_width_half + edge_softness, distance); + + if (stroke_width_half > 0.0) + { + float color_blend = smoothstep(-stroke_width_half - edge_softness, -stroke_width_half + edge_softness, distance); + final_color = mix(fill_color, stroke_color, color_blend); + } + } + + // final color + gl_FragColor = vec4(final_color.rgb, final_color.a * final_alpha); +} diff --git a/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/simple.frag b/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/simple.frag new file mode 100644 index 0000000..106ec49 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/simple.frag @@ -0,0 +1,31 @@ +#version 110 + +uniform sampler2D tex; +uniform float cornerRadius; // in pixels +uniform vec2 size; // in pixels: size of the rendered image quad +uniform vec4 inset; // texture coordinate insets (minX, minY, maxX, maxY) + +varying vec2 fragTexCoord; +varying float fragAlpha; + +void main() { + float alpha = 1.0; + if (cornerRadius > 0.5) { + // normalize texture coords from [insetMin, insetMax] to [0, 1] + // this makes the rounding calculation work correctly for cropped/covered images + vec2 normalizedCoord = (fragTexCoord - inset.xy) / (inset.zw - inset.xy); + + vec2 pos = normalizedCoord * size; + vec2 halfSize = size * 0.5; + float dist = length(max(abs(pos - halfSize) - halfSize + cornerRadius, 0.0)) - cornerRadius; + alpha = 1.0 - smoothstep(-1.0, 1.0, dist); + } + + vec4 texColor = texture2D(tex, fragTexCoord); + texColor.a *= fragAlpha * alpha; + texColor.rgb *= fragAlpha * alpha; + + if(texColor.a < 0.01) + discard; + gl_FragColor = texColor; +} diff --git a/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/simple.vert b/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/simple.vert new file mode 100644 index 0000000..d76c898 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/simple.vert @@ -0,0 +1,16 @@ +#version 110 + +uniform float alpha; + +attribute vec3 vert; +attribute vec2 vertTexCoord; + +varying vec2 fragTexCoord; +varying float fragAlpha; + +void main() { + fragTexCoord = vertTexCoord; + fragAlpha = alpha; + + gl_Position = vec4(vert, 1); +} \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/simple_es.frag b/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/simple_es.frag new file mode 100644 index 0000000..4fc43aa --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/simple_es.frag @@ -0,0 +1,41 @@ +#version 100 + +#ifdef GL_ES +# ifdef GL_FRAGMENT_PRECISION_HIGH +precision highp float; +# else +precision mediump float; +#endif +precision mediump int; +precision lowp sampler2D; +#endif + +uniform sampler2D tex; +uniform float cornerRadius; // in pixels +uniform vec2 size; // in pixels: size of the rendered image quad +uniform vec4 inset; // texture coordinate insets (minX, minY, maxX, maxY) + +varying vec2 fragTexCoord; +varying float fragAlpha; + +void main() { + float alpha = 1.0; + if (cornerRadius > 0.5) { + // normalize texture coords from [insetMin, insetMax] to [0, 1] + // this makes the rounding calculation work correctly for cropped/covered images + vec2 normalizedCoord = (fragTexCoord - inset.xy) / (inset.zw - inset.xy); + + vec2 pos = normalizedCoord * size; + vec2 halfSize = size * 0.5; + float dist = length(max(abs(pos - halfSize) - halfSize + cornerRadius, 0.0)) - cornerRadius; + alpha = 1.0 - smoothstep(-1.0, 1.0, dist); + } + + vec4 texColor = texture2D(tex, fragTexCoord); + texColor.a *= fragAlpha * alpha; + texColor.rgb *= fragAlpha * alpha; + + if(texColor.a < 0.01) + discard; + gl_FragColor = texColor; +} diff --git a/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/simple_es.vert b/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/simple_es.vert new file mode 100644 index 0000000..d94769b --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders/simple_es.vert @@ -0,0 +1,26 @@ +#version 100 + +#ifdef GL_ES +# ifdef GL_FRAGMENT_PRECISION_HIGH +precision highp float; +# else +precision mediump float; +#endif +precision mediump int; +precision lowp sampler2D; +#endif + +uniform float alpha; + +attribute vec3 vert; +attribute vec2 vertTexCoord; + +varying vec2 fragTexCoord; +varying float fragAlpha; + +void main() { + fragTexCoord = vertTexCoord; + fragAlpha = alpha; + + gl_Position = vec4(vert, 1); +} \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders_es.go b/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders_es.go new file mode 100644 index 0000000..f16fa85 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/painter/gl/shaders_es.go @@ -0,0 +1,52 @@ +//go:build ((gles || arm || arm64) && !android && !ios && !mobile && !darwin && !wasm && !test_web_driver) || ((android || ios || mobile) && (!wasm || !test_web_driver)) || wasm || test_web_driver + +package gl + +import _ "embed" + +var ( + //go:embed shaders/line_es.frag + shaderLineesFrag []byte + + //go:embed shaders/line_es.vert + shaderLineesVert []byte + + //go:embed shaders/rectangle_es.frag + shaderRectangleesFrag []byte + + //go:embed shaders/rectangle_es.vert + shaderRectangleesVert []byte + + //go:embed shaders/round_rectangle_es.frag + shaderRoundrectangleesFrag []byte + + //go:embed shaders/simple_es.frag + shaderSimpleesFrag []byte + + //go:embed shaders/simple_es.vert + shaderSimpleesVert []byte + + //go:embed shaders/polygon_es.frag + shaderPolygonesFrag []byte + + //go:embed shaders/arc_es.frag + shaderArcesFrag []byte +) + +func shaderSourceNamed(name string) ([]byte, []byte) { + switch name { + case "line_es": + return shaderLineesVert, shaderLineesFrag + case "simple_es": + return shaderSimpleesVert, shaderSimpleesFrag + case "rectangle_es": + return shaderRectangleesVert, shaderRectangleesFrag + case "round_rectangle_es": + return shaderRectangleesVert, shaderRoundrectangleesFrag + case "polygon_es": + return shaderRectangleesVert, shaderPolygonesFrag + case "arc_es": + return shaderRectangleesVert, shaderArcesFrag + } + return nil, nil +} diff --git a/vendor/fyne.io/fyne/v2/internal/painter/gl/texture.go b/vendor/fyne.io/fyne/v2/internal/painter/gl/texture.go new file mode 100644 index 0000000..5cfda8b --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/painter/gl/texture.go @@ -0,0 +1,210 @@ +package gl + +import ( + "errors" + "fmt" + "image" + "image/draw" + "math" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/internal/cache" + paint "fyne.io/fyne/v2/internal/painter" + "fyne.io/fyne/v2/theme" +) + +const floatEqualityThreshold = 1e-9 + +var noTexture = Texture(cache.NoTexture) + +// Texture represents an uploaded GL texture +type Texture cache.TextureType + +func (p *painter) freeTexture(obj fyne.CanvasObject) { + texture, ok := cache.GetTexture(obj) + if !ok { + return + } + + p.ctx.DeleteTexture(Texture(texture)) + p.logError() + cache.DeleteTexture(obj) +} + +func (p *painter) getTexture(object fyne.CanvasObject, creator func(canvasObject fyne.CanvasObject) Texture) (Texture, error) { + if t, ok := object.(*canvas.Text); ok { + custom := "" + if t.FontSource != nil { + custom = t.FontSource.Name() + } + ent := cache.FontCacheEntry{Color: t.Color, Canvas: p.canvas} + ent.Text = t.Text + ent.Size = t.TextSize + ent.Style = t.TextStyle + ent.Source = custom + + texture, ok := cache.GetTextTexture(ent) + + if !ok { + tex := creator(object) + texture = cache.TextureType(tex) + cache.SetTextTexture(ent, texture, p.canvas, func() { + p.ctx.DeleteTexture(tex) + }) + } + + return Texture(texture), nil + } + + texture, ok := cache.GetTexture(object) + + if !ok { + texture = cache.TextureType(creator(object)) + cache.SetTexture(object, texture, p.canvas) + } + if !cache.IsValid(texture) { + return noTexture, errors.New("no texture available") + } + return Texture(texture), nil +} + +func (p *painter) imgToTexture(img image.Image, textureFilter canvas.ImageScale) Texture { + switch i := img.(type) { + case *image.Uniform: + texture := p.newTexture(textureFilter) + r, g, b, a := i.RGBA() + r8, g8, b8, a8 := uint8(r>>8), uint8(g>>8), uint8(b>>8), uint8(a>>8) + data := []uint8{r8, g8, b8, a8} + p.ctx.TexImage2D( + texture2D, + 0, + 1, + 1, + colorFormatRGBA, + unsignedByte, + data, + ) + p.logError() + return texture + case *image.RGBA: + if len(i.Pix) == 0 { // image is empty + return noTexture + } + + texture := p.newTexture(textureFilter) + p.ctx.TexImage2D( + texture2D, + 0, + i.Rect.Size().X, + i.Rect.Size().Y, + colorFormatRGBA, + unsignedByte, + i.Pix, + ) + p.logError() + return texture + default: + rgba := image.NewRGBA(image.Rect(0, 0, img.Bounds().Dx(), img.Bounds().Dy())) + draw.Draw(rgba, rgba.Rect, img, image.Point{}, draw.Over) + return p.imgToTexture(rgba, textureFilter) + } +} + +func (p *painter) newGlImageTexture(obj fyne.CanvasObject) Texture { + img := obj.(*canvas.Image) + + width := p.textureScale(img.Size().Width) + height := p.textureScale(img.Size().Height) + + tex := paint.PaintImage(img, p.canvas, int(width), int(height)) + if tex == nil { + return noTexture + } + + return p.imgToTexture(tex, img.ScaleMode) +} + +func (p *painter) newGlLinearGradientTexture(obj fyne.CanvasObject) Texture { + gradient := obj.(*canvas.LinearGradient) + + w := gradient.Size().Width + h := gradient.Size().Height + switch a := gradient.Angle; { + case almostEqual(a, 90), almostEqual(a, 270): + h = 1 + case almostEqual(a, 0), almostEqual(a, 180): + w = 1 + } + width := p.textureScale(w) + height := p.textureScale(h) + + return p.imgToTexture(gradient.Generate(int(width), int(height)), canvas.ImageScaleSmooth) +} + +func (p *painter) newGlRadialGradientTexture(obj fyne.CanvasObject) Texture { + gradient := obj.(*canvas.RadialGradient) + + width := p.textureScale(gradient.Size().Width) + height := p.textureScale(gradient.Size().Height) + + return p.imgToTexture(gradient.Generate(int(width), int(height)), canvas.ImageScaleSmooth) +} + +func (p *painter) newGlRasterTexture(obj fyne.CanvasObject) Texture { + rast := obj.(*canvas.Raster) + + width := p.textureScale(rast.Size().Width) + height := p.textureScale(rast.Size().Height) + + return p.imgToTexture(rast.Generator(int(width), int(height)), rast.ScaleMode) +} + +func (p *painter) newGlTextTexture(obj fyne.CanvasObject) Texture { + text := obj.(*canvas.Text) + color := text.Color + if color == nil { + color = theme.Color(theme.ColorNameForeground) + } + + bounds := text.MinSize() + width := int(math.Ceil(float64(p.textureScale(bounds.Width) + paint.VectorPad(text)))) // potentially italic overspill + height := int(math.Ceil(float64(p.textureScale(bounds.Height)))) + img := image.NewNRGBA(image.Rect(0, 0, width, height)) + + face := paint.CachedFontFace(text.TextStyle, text.FontSource, text) + paint.DrawString(img, text.Text, color, face.Fonts, text.TextSize, p.pixScale, text.TextStyle) + return p.imgToTexture(img, canvas.ImageScaleSmooth) +} + +func (p *painter) newTexture(textureFilter canvas.ImageScale) Texture { + if int(textureFilter) >= len(textureFilterToGL) { + fyne.LogError(fmt.Sprintf("Invalid canvas.ImageScale value (%d), using canvas.ImageScaleSmooth as default value", textureFilter), nil) + textureFilter = canvas.ImageScaleSmooth + } + + texture := p.ctx.CreateTexture() + p.logError() + p.ctx.ActiveTexture(texture0) + p.ctx.BindTexture(texture2D, texture) + p.logError() + p.ctx.TexParameteri(texture2D, textureMinFilter, textureFilterToGL[textureFilter]) + p.ctx.TexParameteri(texture2D, textureMagFilter, textureFilterToGL[textureFilter]) + p.ctx.TexParameteri(texture2D, textureWrapS, clampToEdge) + p.ctx.TexParameteri(texture2D, textureWrapT, clampToEdge) + p.logError() + + return texture +} + +func (p *painter) textureScale(v float32) float32 { + if p.pixScale == 1.0 { + return float32(math.Round(float64(v))) + } + + return float32(math.Round(float64(v * p.pixScale))) +} + +func almostEqual(a, b float64) bool { + return math.Abs(a-b) < floatEqualityThreshold +} diff --git a/vendor/fyne.io/fyne/v2/internal/painter/image.go b/vendor/fyne.io/fyne/v2/internal/painter/image.go new file mode 100644 index 0000000..55dbb3a --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/painter/image.go @@ -0,0 +1,68 @@ +package painter + +import ( + "image" + _ "image/jpeg" // avoid users having to import when using image widget + _ "image/png" // avoid the same for PNG images + + "golang.org/x/image/draw" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" +) + +// PaintImage renders a given fyne Image to a Go standard image +// If a fyne.Canvas is given and the image’s fill mode is “fill original” the image’s min size has +// to fit its original size. If it doesn’t, PaintImage does not paint the image but adjusts its min size. +// The image will then be painted on the next frame because of the min size change. +func PaintImage(img *canvas.Image, c fyne.Canvas, width, height int) image.Image { + if img.Size().IsZero() && c == nil { // an image without size or canvas won't get rendered unless we setup + img.Resize(fyne.NewSize(float32(width), float32(height))) + } + dst, err := paintImage(img, width, height) + if err != nil { + fyne.LogError("failed to paint image", err) + } + + return dst +} + +func paintImage(img *canvas.Image, width, height int) (dst image.Image, err error) { + if width <= 0 || height <= 0 { + return dst, err + } + + dst = img.Image + if dst == nil { + dst = image.NewNRGBA(image.Rect(0, 0, width, height)) + } + + size := dst.Bounds().Size() + if width != size.X || height != size.Y { + dst = scaleImage(dst, width, height, img.ScaleMode) + } + return dst, err +} + +func scaleImage(pixels image.Image, scaledW, scaledH int, scale canvas.ImageScale) image.Image { + if scale == canvas.ImageScaleFastest || scale == canvas.ImageScalePixels { + // do not perform software scaling + return pixels + } + + bounds := pixels.Bounds() + pixW := int(fyne.Min(float32(scaledW), float32(bounds.Dx()))) // don't push more pixels than we have to + pixH := int(fyne.Min(float32(scaledH), float32(bounds.Dy()))) // the GL calls will scale this up on GPU. + scaledBounds := image.Rect(0, 0, pixW, pixH) + tex := image.NewNRGBA(scaledBounds) + switch scale { + case canvas.ImageScalePixels: + draw.NearestNeighbor.Scale(tex, scaledBounds, pixels, bounds, draw.Over, nil) + default: + if scale != canvas.ImageScaleSmooth { + fyne.LogError("Invalid canvas.ImageScale value, using canvas.ImageScaleSmooth", nil) + } + draw.CatmullRom.Scale(tex, scaledBounds, pixels, bounds, draw.Over, nil) + } + return tex +} diff --git a/vendor/fyne.io/fyne/v2/internal/painter/software/draw.go b/vendor/fyne.io/fyne/v2/internal/painter/software/draw.go new file mode 100644 index 0000000..77b08a8 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/painter/software/draw.go @@ -0,0 +1,376 @@ +package software + +import ( + "fmt" + "image" + "image/color" + "math" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/internal/painter" + "fyne.io/fyne/v2/internal/scale" + "fyne.io/fyne/v2/theme" + + "golang.org/x/image/draw" +) + +type gradient interface { + Generate(int, int) image.Image + Size() fyne.Size +} + +func drawArc(c fyne.Canvas, arc *canvas.Arc, pos fyne.Position, base *image.NRGBA, clip image.Rectangle) { + pad := painter.VectorPad(arc) + scaledWidth := scale.ToScreenCoordinate(c, arc.Size().Width+pad*2) + scaledHeight := scale.ToScreenCoordinate(c, arc.Size().Height+pad*2) + scaledX, scaledY := scale.ToScreenCoordinate(c, pos.X-pad), scale.ToScreenCoordinate(c, pos.Y-pad) + bounds := clip.Intersect(image.Rect(scaledX, scaledY, scaledX+scaledWidth, scaledY+scaledHeight)) + + raw := painter.DrawArc(arc, pad, func(in float32) float32 { + return float32(math.Round(float64(in) * float64(c.Scale()))) + }) + + // the clip intersect above cannot be negative, so we may need to compensate + offX, offY := 0, 0 + if scaledX < 0 { + offX = -scaledX + } + if scaledY < 0 { + offY = -scaledY + } + draw.Draw(base, bounds, raw, image.Point{offX, offY}, draw.Over) +} + +func drawCircle(c fyne.Canvas, circle *canvas.Circle, pos fyne.Position, base *image.NRGBA, clip image.Rectangle) { + pad := painter.VectorPad(circle) + scaledWidth := scale.ToScreenCoordinate(c, circle.Size().Width+pad*2) + scaledHeight := scale.ToScreenCoordinate(c, circle.Size().Height+pad*2) + scaledX, scaledY := scale.ToScreenCoordinate(c, pos.X-pad), scale.ToScreenCoordinate(c, pos.Y-pad) + bounds := clip.Intersect(image.Rect(scaledX, scaledY, scaledX+scaledWidth, scaledY+scaledHeight)) + + raw := painter.DrawCircle(circle, pad, func(in float32) float32 { + return float32(math.Round(float64(in) * float64(c.Scale()))) + }) + + // the clip intersect above cannot be negative, so we may need to compensate + offX, offY := 0, 0 + if scaledX < 0 { + offX = -scaledX + } + if scaledY < 0 { + offY = -scaledY + } + draw.Draw(base, bounds, raw, image.Point{offX, offY}, draw.Over) +} + +func drawGradient(c fyne.Canvas, g gradient, pos fyne.Position, base *image.NRGBA, clip image.Rectangle) { + bounds := g.Size() + width := scale.ToScreenCoordinate(c, bounds.Width) + height := scale.ToScreenCoordinate(c, bounds.Height) + tex := g.Generate(width, height) + drawTex(scale.ToScreenCoordinate(c, pos.X), scale.ToScreenCoordinate(c, pos.Y), width, height, base, tex, clip, 1.0) +} + +func drawImage(c fyne.Canvas, img *canvas.Image, pos fyne.Position, base *image.NRGBA, clip image.Rectangle) { + bounds := img.Size() + if bounds.IsZero() { + return + } + width := scale.ToScreenCoordinate(c, bounds.Width) + height := scale.ToScreenCoordinate(c, bounds.Height) + scaledX, scaledY := scale.ToScreenCoordinate(c, pos.X), scale.ToScreenCoordinate(c, pos.Y) + + origImg := img.Image + if img.FillMode != canvas.ImageFillCover { + origImg = painter.PaintImage(img, c, width, height) + } + + if img.FillMode == canvas.ImageFillContain { + imgAspect := img.Aspect() + objAspect := float32(width) / float32(height) + + if objAspect > imgAspect { + newWidth := int(float32(height) * imgAspect) + scaledX += (width - newWidth) / 2 + width = newWidth + } else if objAspect < imgAspect { + newHeight := int(float32(width) / imgAspect) + scaledY += (height - newHeight) / 2 + height = newHeight + } + } else if img.FillMode == canvas.ImageFillCover { + inner := origImg.Bounds() + imgAspect := img.Aspect() + objAspect := float32(width) / float32(height) + + if objAspect > imgAspect { + newHeight := float32(width) / imgAspect + heightPad := (newHeight - float32(height)) / 2 + pixPad := int((heightPad / newHeight) * float32(inner.Dy())) + + inner = image.Rect(inner.Min.X, inner.Min.Y+pixPad, inner.Max.X, inner.Max.Y-pixPad) + } else if objAspect < imgAspect { + newWidth := float32(height) * imgAspect + widthPad := (newWidth - float32(width)) / 2 + pixPad := int((widthPad / newWidth) * float32(inner.Dx())) + + inner = image.Rect(inner.Min.X+pixPad, inner.Min.Y, inner.Max.X-pixPad, inner.Max.Y) + } + + subImg := image.NewRGBA(inner.Bounds()) + draw.Copy(subImg, inner.Min, origImg, inner, draw.Over, nil) + origImg = subImg + } + + cornerRadius := fyne.Min(painter.GetMaximumRadius(bounds), img.CornerRadius) + drawPixels(scaledX, scaledY, width, height, img.ScaleMode, base, origImg, clip, img.Alpha(), cornerRadius*c.Scale()) +} + +func drawPixels(x, y, width, height int, mode canvas.ImageScale, base *image.NRGBA, origImg image.Image, clip image.Rectangle, alpha float64, radius float32) { + if origImg.Bounds().Dx() == width && origImg.Bounds().Dy() == height && radius < 0.5 { + // do not scale or duplicate image since not needed, draw directly + drawTex(x, y, width, height, base, origImg, clip, alpha) + return + } + + scaledBounds := image.Rect(0, 0, width, height) + scaledImg := image.NewNRGBA(scaledBounds) + switch mode { + case canvas.ImageScalePixels: + draw.NearestNeighbor.Scale(scaledImg, scaledBounds, origImg, origImg.Bounds(), draw.Over, nil) + case canvas.ImageScaleFastest: + draw.ApproxBiLinear.Scale(scaledImg, scaledBounds, origImg, origImg.Bounds(), draw.Over, nil) + default: + if mode != canvas.ImageScaleSmooth { + fyne.LogError(fmt.Sprintf("Invalid canvas.ImageScale value (%d), using canvas.ImageScaleSmooth as default value", mode), nil) + } + draw.CatmullRom.Scale(scaledImg, scaledBounds, origImg, origImg.Bounds(), draw.Over, nil) + } + + if radius > 0.5 { + applyRoundedCorners(scaledImg, width, height, radius) + } + + drawTex(x, y, width, height, base, scaledImg, clip, alpha) +} + +func drawLine(c fyne.Canvas, line *canvas.Line, pos fyne.Position, base *image.NRGBA, clip image.Rectangle) { + pad := painter.VectorPad(line) + scaledWidth := scale.ToScreenCoordinate(c, line.Size().Width+pad*2) + scaledHeight := scale.ToScreenCoordinate(c, line.Size().Height+pad*2) + scaledX, scaledY := scale.ToScreenCoordinate(c, pos.X-pad), scale.ToScreenCoordinate(c, pos.Y-pad) + bounds := clip.Intersect(image.Rect(scaledX, scaledY, scaledX+scaledWidth, scaledY+scaledHeight)) + + raw := painter.DrawLine(line, pad, func(in float32) float32 { + return float32(math.Round(float64(in) * float64(c.Scale()))) + }) + + // the clip intersect above cannot be negative, so we may need to compensate + offX, offY := 0, 0 + if scaledX < 0 { + offX = -scaledX + } + if scaledY < 0 { + offY = -scaledY + } + draw.Draw(base, bounds, raw, image.Point{offX, offY}, draw.Over) +} + +func drawTex(x, y, width, height int, base *image.NRGBA, tex image.Image, clip image.Rectangle, alpha float64) { + outBounds := image.Rect(x, y, x+width, y+height) + clippedBounds := clip.Intersect(outBounds) + srcPt := image.Point{X: clippedBounds.Min.X - outBounds.Min.X, Y: clippedBounds.Min.Y - outBounds.Min.Y} + if alpha == 1.0 { + draw.Draw(base, clippedBounds, tex, srcPt, draw.Over) + } else { + mask := &image.Uniform{C: color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: uint8(float64(0xff) * alpha)}} + draw.DrawMask(base, clippedBounds, tex, srcPt, mask, srcPt, draw.Over) + } +} + +func drawText(c fyne.Canvas, text *canvas.Text, pos fyne.Position, base *image.NRGBA, clip image.Rectangle) { + bounds := text.MinSize() + width := scale.ToScreenCoordinate(c, bounds.Width+painter.VectorPad(text)) + height := scale.ToScreenCoordinate(c, bounds.Height) + txtImg := image.NewRGBA(image.Rect(0, 0, width, height)) + + color := text.Color + if color == nil { + color = theme.Color(theme.ColorNameForeground) + } + + face := painter.CachedFontFace(text.TextStyle, text.FontSource, text) + painter.DrawString(txtImg, text.Text, color, face.Fonts, text.TextSize, c.Scale(), text.TextStyle) + + size := text.Size() + offsetX := float32(0) + offsetY := float32(0) + switch text.Alignment { + case fyne.TextAlignTrailing: + offsetX = size.Width - bounds.Width + case fyne.TextAlignCenter: + offsetX = (size.Width - bounds.Width) / 2 + } + if size.Height > bounds.Height { + offsetY = (size.Height - bounds.Height) / 2 + } + scaledX := scale.ToScreenCoordinate(c, pos.X+offsetX) + scaledY := scale.ToScreenCoordinate(c, pos.Y+offsetY) + imgBounds := image.Rect(scaledX, scaledY, scaledX+width, scaledY+height) + clippedBounds := clip.Intersect(imgBounds) + srcPt := image.Point{X: clippedBounds.Min.X - imgBounds.Min.X, Y: clippedBounds.Min.Y - imgBounds.Min.Y} + draw.Draw(base, clippedBounds, txtImg, srcPt, draw.Over) +} + +func drawRaster(c fyne.Canvas, rast *canvas.Raster, pos fyne.Position, base *image.NRGBA, clip image.Rectangle) { + bounds := rast.Size() + if bounds.IsZero() { + return + } + width := scale.ToScreenCoordinate(c, bounds.Width) + height := scale.ToScreenCoordinate(c, bounds.Height) + scaledX, scaledY := scale.ToScreenCoordinate(c, pos.X), scale.ToScreenCoordinate(c, pos.Y) + + pix := rast.Generator(width, height) + if pix.Bounds().Bounds().Dx() != width || pix.Bounds().Dy() != height { + drawPixels(scaledX, scaledY, width, height, rast.ScaleMode, base, pix, clip, 1.0, 0.0) + } else { + drawTex(scaledX, scaledY, width, height, base, pix, clip, 1.0) + } +} + +func drawOblongStroke(c fyne.Canvas, obj fyne.CanvasObject, width, height float32, pos fyne.Position, base *image.NRGBA, clip image.Rectangle) { + pad := painter.VectorPad(obj) + scaledWidth := scale.ToScreenCoordinate(c, width+pad*2) + scaledHeight := scale.ToScreenCoordinate(c, height+pad*2) + scaledX, scaledY := scale.ToScreenCoordinate(c, pos.X-pad), scale.ToScreenCoordinate(c, pos.Y-pad) + bounds := clip.Intersect(image.Rect(scaledX, scaledY, scaledX+scaledWidth, scaledY+scaledHeight)) + + raw := painter.DrawRectangle(obj.(*canvas.Rectangle), width, height, pad, func(in float32) float32 { + return float32(math.Round(float64(in) * float64(c.Scale()))) + }) + + // the clip intersect above cannot be negative, so we may need to compensate + offX, offY := 0, 0 + if scaledX < 0 { + offX = -scaledX + } + if scaledY < 0 { + offY = -scaledY + } + draw.Draw(base, bounds, raw, image.Point{offX, offY}, draw.Over) +} + +func drawPolygon(c fyne.Canvas, polygon *canvas.Polygon, pos fyne.Position, base *image.NRGBA, clip image.Rectangle) { + pad := painter.VectorPad(polygon) + scaledWidth := scale.ToScreenCoordinate(c, polygon.Size().Width+pad*2) + scaledHeight := scale.ToScreenCoordinate(c, polygon.Size().Height+pad*2) + scaledX, scaledY := scale.ToScreenCoordinate(c, pos.X-pad), scale.ToScreenCoordinate(c, pos.Y-pad) + bounds := clip.Intersect(image.Rect(scaledX, scaledY, scaledX+scaledWidth, scaledY+scaledHeight)) + + raw := painter.DrawPolygon(polygon, pad, func(in float32) float32 { + return float32(math.Round(float64(in) * float64(c.Scale()))) + }) + + // the clip intersect above cannot be negative, so we may need to compensate + offX, offY := 0, 0 + if scaledX < 0 { + offX = -scaledX + } + if scaledY < 0 { + offY = -scaledY + } + draw.Draw(base, bounds, raw, image.Point{offX, offY}, draw.Over) +} + +func drawRectangle(c fyne.Canvas, rect *canvas.Rectangle, pos fyne.Position, base *image.NRGBA, clip image.Rectangle) { + topRightRadius := painter.GetCornerRadius(rect.TopRightCornerRadius, rect.CornerRadius) + topLeftRadius := painter.GetCornerRadius(rect.TopLeftCornerRadius, rect.CornerRadius) + bottomRightRadius := painter.GetCornerRadius(rect.BottomRightCornerRadius, rect.CornerRadius) + bottomLeftRadius := painter.GetCornerRadius(rect.BottomLeftCornerRadius, rect.CornerRadius) + drawOblong(c, rect, rect.FillColor, rect.StrokeColor, rect.StrokeWidth, topRightRadius, topLeftRadius, bottomRightRadius, bottomLeftRadius, rect.Aspect, pos, base, clip) +} + +func drawOblong(c fyne.Canvas, obj fyne.CanvasObject, fill, stroke color.Color, strokeWidth, topRightRadius, topLeftRadius, bottomRightRadius, bottomLeftRadius, aspect float32, pos fyne.Position, base *image.NRGBA, clip image.Rectangle) { + width, height := obj.Size().Components() + if aspect != 0 { + frameAspect := width / height + + xPad, yPad := float32(0), float32(0) + if frameAspect > aspect { + newWidth := height * aspect + xPad = (width - newWidth) / 2 + width = newWidth + } else if frameAspect < aspect { + newHeight := width / aspect + yPad = (height - newHeight) / 2 + height = newHeight + } + + pos = pos.AddXY(xPad, yPad) + } + + if (stroke != nil && strokeWidth > 0) || topRightRadius != 0 || topLeftRadius != 0 || bottomRightRadius != 0 || bottomLeftRadius != 0 { // use a rasterizer if there is a stroke or radius + drawOblongStroke(c, obj, width, height, pos, base, clip) + return + } + + scaledWidth := scale.ToScreenCoordinate(c, width) + scaledHeight := scale.ToScreenCoordinate(c, height) + scaledX, scaledY := scale.ToScreenCoordinate(c, pos.X), scale.ToScreenCoordinate(c, pos.Y) + bounds := clip.Intersect(image.Rect(scaledX, scaledY, scaledX+scaledWidth, scaledY+scaledHeight)) + draw.Draw(base, bounds, image.NewUniform(fill), image.Point{}, draw.Over) +} + +// applyRoundedCorners rounds the corners of the image in-place +func applyRoundedCorners(img *image.NRGBA, w, h int, radius float32) { + rInt := int(math.Ceil(float64(radius))) + + aaWidth := float32(0.5) + outerR2 := (radius + aaWidth) * (radius + aaWidth) + innerR2 := (radius - aaWidth) * (radius - aaWidth) + + applyCorner := func(startX, endX, startY, endY int, cx, cy float32) { + for y := startY; y < endY; y++ { + for x := startX; x < endX; x++ { + dx := float32(x) - cx + dy := float32(y) - cy + dist2 := dx*dx + dy*dy + + i := img.PixOffset(x, y) + alpha := img.Pix[i+3] + + switch { + case dist2 >= outerR2: + img.Pix[i+3] = 0 // Fully transparent + case dist2 > innerR2: + // Linear falloff based on squared distance + t := (outerR2 - dist2) / (outerR2 - innerR2) // t ranges from 0 to 1 + newAlpha := uint8(float32(alpha) * t) + img.Pix[i+3] = newAlpha + } + } + } + } + + // Top-left + r := minInt(rInt, minInt(w, h)) + applyCorner(0, r, 0, r, radius, radius) + + // Top-right + applyCorner(w-r, w, 0, r, float32(w)-radius, radius) + + // Bottom-left + applyCorner(0, r, h-r, h, radius, float32(h)-radius) + + // Bottom-right + applyCorner(w-r, w, h-r, h, float32(w)-radius, float32(h)-radius) +} + +func minInt(x, y int) int { + if x < y { + return x + } + return y +} diff --git a/vendor/fyne.io/fyne/v2/internal/painter/software/painter.go b/vendor/fyne.io/fyne/v2/internal/painter/software/painter.go new file mode 100644 index 0000000..c75e712 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/painter/software/painter.go @@ -0,0 +1,66 @@ +package software + +import ( + "image" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/internal/driver" + "fyne.io/fyne/v2/internal/scale" +) + +// Painter is a simple software painter that can paint a canvas in memory. +type Painter struct{} + +// NewPainter creates a new Painter. +func NewPainter() *Painter { + return &Painter{} +} + +// Paint is the main entry point for a simple software painter. +// The canvas to be drawn is passed in as a parameter and the return is an +// image containing the result of rendering. +func (*Painter) Paint(c fyne.Canvas) image.Image { + bounds := image.Rect(0, 0, scale.ToScreenCoordinate(c, c.Size().Width), scale.ToScreenCoordinate(c, c.Size().Height)) + base := image.NewNRGBA(bounds) + + paint := func(obj fyne.CanvasObject, pos, clipPos fyne.Position, clipSize fyne.Size) bool { + w := fyne.Min(clipPos.X+clipSize.Width, c.Size().Width) + h := fyne.Min(clipPos.Y+clipSize.Height, c.Size().Height) + clip := image.Rect( + scale.ToScreenCoordinate(c, clipPos.X), + scale.ToScreenCoordinate(c, clipPos.Y), + scale.ToScreenCoordinate(c, w), + scale.ToScreenCoordinate(c, h), + ) + switch o := obj.(type) { + case *canvas.Image: + drawImage(c, o, pos, base, clip) + case *canvas.Text: + drawText(c, o, pos, base, clip) + case gradient: + drawGradient(c, o, pos, base, clip) + case *canvas.Circle: + drawCircle(c, o, pos, base, clip) + case *canvas.Line: + drawLine(c, o, pos, base, clip) + case *canvas.Polygon: + drawPolygon(c, o, pos, base, clip) + case *canvas.Raster: + drawRaster(c, o, pos, base, clip) + case *canvas.Rectangle: + drawRectangle(c, o, pos, base, clip) + case *canvas.Arc: + drawArc(c, o, pos, base, clip) + } + + return false + } + + driver.WalkVisibleObjectTree(c.Content(), paint, nil) + for _, o := range c.Overlays().List() { + driver.WalkVisibleObjectTree(o, paint, nil) + } + + return base +} diff --git a/vendor/fyne.io/fyne/v2/internal/painter/vector.go b/vendor/fyne.io/fyne/v2/internal/painter/vector.go new file mode 100644 index 0000000..92f0b32 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/painter/vector.go @@ -0,0 +1,41 @@ +package painter + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" +) + +// VectorPad returns the number of additional points that should be added around a texture. +// This is to accommodate overflow caused by stroke and line endings etc. +// THe result is in fyne.Size type coordinates and should be scaled for output. +func VectorPad(obj fyne.CanvasObject) float32 { + switch co := obj.(type) { + case *canvas.Circle: + if co.StrokeWidth > 0 && co.StrokeColor != nil { + return co.StrokeWidth + 2 + } + return 1 // anti-alias on circle fill + case *canvas.Line: + if co.StrokeWidth > 0 { + return co.StrokeWidth + 2 + } + case *canvas.Polygon: + if co.StrokeWidth > 0 && co.StrokeColor != nil { + return co.StrokeWidth + 2 + } + case *canvas.Rectangle: + if co.StrokeWidth > 0 && co.StrokeColor != nil { + return co.StrokeWidth + 2 + } + case *canvas.Text: + if co.TextStyle.Italic { + return co.TextSize / 5 // make sure that even a 20% lean does not overflow + } + case *canvas.Arc: + if co.StrokeWidth > 0 && co.StrokeColor != nil { + return co.StrokeWidth + 2 + } + } + + return 0 +} diff --git a/vendor/fyne.io/fyne/v2/internal/preferences.go b/vendor/fyne.io/fyne/v2/internal/preferences.go new file mode 100644 index 0000000..4e76239 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/preferences.go @@ -0,0 +1,316 @@ +package internal + +import ( + "reflect" + "sync" + + "fyne.io/fyne/v2" +) + +// InMemoryPreferences provides an implementation of the fyne.Preferences API that is stored in memory. +type InMemoryPreferences struct { + values map[string]any + lock sync.RWMutex + changeListeners []func() +} + +// Declare conformity with Preferences interface +var _ fyne.Preferences = (*InMemoryPreferences)(nil) + +// AddChangeListener allows code to be notified when some preferences change. This will fire on any update. +// The passed 'listener' should not try to write values. +func (p *InMemoryPreferences) AddChangeListener(listener func()) { + p.lock.Lock() + defer p.lock.Unlock() + + p.changeListeners = append(p.changeListeners, listener) +} + +// Bool looks up a boolean value for the key +func (p *InMemoryPreferences) Bool(key string) bool { + return p.BoolWithFallback(key, false) +} + +func (p *InMemoryPreferences) BoolList(key string) []bool { + return p.BoolListWithFallback(key, []bool{}) +} + +func (p *InMemoryPreferences) BoolListWithFallback(key string, fallback []bool) []bool { + value, ok := p.get(key) + if !ok { + return fallback + } + valb, ok := value.([]bool) + if !ok { + return fallback + } + return valb +} + +// BoolWithFallback looks up a boolean value and returns the given fallback if not found +func (p *InMemoryPreferences) BoolWithFallback(key string, fallback bool) bool { + value, ok := p.get(key) + if !ok { + return fallback + } + valb, ok := value.(bool) + if !ok { + return fallback + } + return valb +} + +// ChangeListeners returns the list of listeners registered for this set of preferences. +func (p *InMemoryPreferences) ChangeListeners() []func() { + return p.changeListeners +} + +// Float looks up a float64 value for the key +func (p *InMemoryPreferences) Float(key string) float64 { + return p.FloatWithFallback(key, 0.0) +} + +func (p *InMemoryPreferences) FloatList(key string) []float64 { + return p.FloatListWithFallback(key, []float64{}) +} + +func (p *InMemoryPreferences) FloatListWithFallback(key string, fallback []float64) []float64 { + value, ok := p.get(key) + if !ok { + return fallback + } + valf, ok := value.([]float64) + if ok { + return valf + } + vali, ok := value.([]int) + if ok { + flts := make([]float64, len(vali)) + for i, f := range vali { + flts[i] = float64(f) + } + return flts + } + return fallback +} + +// FloatWithFallback looks up a float64 value and returns the given fallback if not found +func (p *InMemoryPreferences) FloatWithFallback(key string, fallback float64) float64 { + value, ok := p.get(key) + if !ok { + return fallback + } + valf, ok := value.(float64) + if ok { + return valf + } + vali, ok := value.(int) + if ok { + return float64(vali) + } + return fallback +} + +// Int looks up an integer value for the key +func (p *InMemoryPreferences) Int(key string) int { + return p.IntWithFallback(key, 0) +} + +func (p *InMemoryPreferences) IntList(key string) []int { + return p.IntListWithFallback(key, []int{}) +} + +func (p *InMemoryPreferences) IntListWithFallback(key string, fallback []int) []int { + value, ok := p.get(key) + if !ok { + return fallback + } + vali, ok := value.([]int) + if ok { + return vali + } + // integers can be de-serialised as floats, so support both + valf, ok := value.([]float64) + if ok { + ints := make([]int, len(valf)) + for i, f := range valf { + ints[i] = int(f) + } + return ints + } + return fallback +} + +// IntWithFallback looks up an integer value and returns the given fallback if not found +func (p *InMemoryPreferences) IntWithFallback(key string, fallback int) int { + value, ok := p.get(key) + if !ok { + return fallback + } + vali, ok := value.(int) + if ok { + return vali + } + // integers can be de-serialised as floats, so support both + valf, ok := value.(float64) + if !ok { + return fallback + } + return int(valf) +} + +// ReadValues provides read access to the underlying value map - for internal use only... +// You should not retain a reference to the map nor write to the values in the callback function +func (p *InMemoryPreferences) ReadValues(fn func(map[string]any)) { + p.lock.RLock() + fn(p.values) + p.lock.RUnlock() +} + +// RemoveValue deletes a value on the given key +func (p *InMemoryPreferences) RemoveValue(key string) { + p.remove(key) +} + +// SetBool saves a boolean value for the given key +func (p *InMemoryPreferences) SetBool(key string, value bool) { + p.set(key, value) +} + +func (p *InMemoryPreferences) SetBoolList(key string, value []bool) { + p.set(key, value) +} + +// SetFloat saves a float64 value for the given key +func (p *InMemoryPreferences) SetFloat(key string, value float64) { + p.set(key, value) +} + +func (p *InMemoryPreferences) SetFloatList(key string, value []float64) { + p.set(key, value) +} + +// SetInt saves an integer value for the given key +func (p *InMemoryPreferences) SetInt(key string, value int) { + p.set(key, value) +} + +func (p *InMemoryPreferences) SetIntList(key string, value []int) { + p.set(key, value) +} + +// SetString saves a string value for the given key +func (p *InMemoryPreferences) SetString(key string, value string) { + p.set(key, value) +} + +func (p *InMemoryPreferences) SetStringList(key string, value []string) { + p.set(key, value) +} + +// String looks up a string value for the key +func (p *InMemoryPreferences) String(key string) string { + return p.StringWithFallback(key, "") +} + +func (p *InMemoryPreferences) StringList(key string) []string { + return p.StringListWithFallback(key, []string{}) +} + +func (p *InMemoryPreferences) StringListWithFallback(key string, fallback []string) []string { + value, ok := p.get(key) + if !ok { + return fallback + } + vals, ok := value.([]string) + if !ok { + return fallback + } + return vals +} + +// StringWithFallback looks up a string value and returns the given fallback if not found +func (p *InMemoryPreferences) StringWithFallback(key, fallback string) string { + value, ok := p.get(key) + if !ok { + return fallback + } + vals, ok := value.(string) + if !ok { + return fallback + } + return vals +} + +// WriteValues provides write access to the underlying value map - for internal use only... +// You should not retain a reference to the map passed to the callback function +func (p *InMemoryPreferences) WriteValues(fn func(map[string]any)) { + p.lock.Lock() + fn(p.values) + p.lock.Unlock() + + p.fireChange() +} + +// NewInMemoryPreferences creates a new preferences implementation stored in memory +func NewInMemoryPreferences() *InMemoryPreferences { + return &InMemoryPreferences{values: make(map[string]any)} +} + +func (p *InMemoryPreferences) fireChange() { + p.lock.RLock() + listeners := p.changeListeners + p.lock.RUnlock() + + for _, l := range listeners { + l() + } +} + +func (p *InMemoryPreferences) get(key string) (any, bool) { + p.lock.RLock() + defer p.lock.RUnlock() + + v, err := p.values[key] + return v, err +} + +func (p *InMemoryPreferences) remove(key string) { + p.lock.Lock() + delete(p.values, key) + p.lock.Unlock() + + p.fireChange() +} + +func (p *InMemoryPreferences) set(key string, value any) { + p.lock.Lock() + + if reflect.TypeOf(value).Kind() == reflect.Slice { + s := reflect.ValueOf(value) + old := reflect.ValueOf(p.values[key]) + if p.values[key] != nil && s.Len() == old.Len() { + changed := false + for i := 0; i < s.Len(); i++ { + if s.Index(i).Interface() != old.Index(i).Interface() { + changed = true + break + } + } + if !changed { + p.lock.Unlock() + return + } + } + } else { + if stored, ok := p.values[key]; ok && stored == value { + p.lock.Unlock() + return + } + } + + p.values[key] = value + p.lock.Unlock() + + p.fireChange() +} diff --git a/vendor/fyne.io/fyne/v2/internal/repository/file.go b/vendor/fyne.io/fyne/v2/internal/repository/file.go new file mode 100644 index 0000000..02944c9 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/repository/file.go @@ -0,0 +1,338 @@ +package repository + +import ( + "io" + "io/fs" + "os" + "path" + "path/filepath" + "runtime" + "strings" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/storage" + "fyne.io/fyne/v2/storage/repository" +) + +// declare conformance with repository types +var ( + _ repository.Repository = (*FileRepository)(nil) + _ repository.WritableRepository = (*FileRepository)(nil) + _ repository.DeleteAllRepository = (*FileRepository)(nil) + _ repository.AppendableRepository = (*FileRepository)(nil) + _ repository.HierarchicalRepository = (*FileRepository)(nil) + _ repository.ListableRepository = (*FileRepository)(nil) + _ repository.MovableRepository = (*FileRepository)(nil) + _ repository.CopyableRepository = (*FileRepository)(nil) +) + +var ( + _ fyne.URIReadCloser = (*file)(nil) + _ fyne.URIWriteCloser = (*file)(nil) +) + +type file struct { + *os.File + uri fyne.URI +} + +func (f *file) URI() fyne.URI { + return f.uri +} + +// FileRepository implements a simple wrapper around Go's filesystem +// interface libraries. It should be registered by the driver on platforms +// where it is appropriate to do so. +// +// This repository is suitable to handle the file:// scheme. +// +// Since: 2.0 +type FileRepository struct{} + +// NewFileRepository creates a new FileRepository instance. +// The caller needs to call repository.Register() with the result of this function. +// +// Since: 2.0 +func NewFileRepository() *FileRepository { + return &FileRepository{} +} + +// Exists checks if the given URI exists. +// +// Since: 2.0 +func (r *FileRepository) Exists(u fyne.URI) (bool, error) { + p := u.Path() + _, err := os.Stat(p) + if err == nil { + return true, nil + } else if os.IsNotExist(err) { + return false, nil + } + + return false, err +} + +// Reader returns a reader for the given URI. +// +// Since: 2.0 +func (r *FileRepository) Reader(u fyne.URI) (fyne.URIReadCloser, error) { + return openFile(u, false, false) +} + +// CanRead checks if the given URI can be read. +// +// Since: 2.0 +func (r *FileRepository) CanRead(u fyne.URI) (bool, error) { + f, err := os.OpenFile(u.Path(), os.O_RDONLY, 0o666) + if err != nil { + if os.IsPermission(err) || os.IsNotExist(err) { + return false, nil + } + + return false, err + } + + return true, f.Close() +} + +// Destroy tears down the repository for the specified scheme. +func (r *FileRepository) Destroy(scheme string) { + // do nothing +} + +// Writer returns a truncating writer for the given URI. +// +// Since: 2.0 +func (r *FileRepository) Writer(u fyne.URI) (fyne.URIWriteCloser, error) { + return openFile(u, true, true) +} + +// Appender returns a writer that appends to the given URI. +// +// Since: 2.6 +func (r *FileRepository) Appender(u fyne.URI) (fyne.URIWriteCloser, error) { + return openFile(u, true, false) +} + +// CanWrite checks if the given URI can be written. +// +// Since: 2.0 +func (r *FileRepository) CanWrite(u fyne.URI) (bool, error) { + f, err := os.OpenFile(u.Path(), os.O_WRONLY, 0o666) + if err != nil { + if os.IsPermission(err) { + return false, nil + } + + if os.IsNotExist(err) { + // We may need to do extra logic to check if the + // directory is writable, but presumably the + // IsPermission check covers this. + return true, nil + } + + return false, err + } + + return true, f.Close() +} + +// Delete deletes the given URI. +// +// Since: 2.0 +func (r *FileRepository) Delete(u fyne.URI) error { + return os.Remove(u.Path()) +} + +// DeleteAll deletes the given URI and all its children. +// +// Since: 2.7 +func (r *FileRepository) DeleteAll(u fyne.URI) error { + return os.RemoveAll(u.Path()) +} + +// Parent returns the parent URI of the given URI. +// +// Since: 2.0 +func (r *FileRepository) Parent(u fyne.URI) (fyne.URI, error) { + child := path.Clean(u.Path()) + if child == "." || // Clean ending up empty returns ".". + strings.HasSuffix(child, "/") || // Only root has trailing slash. + runtime.GOOS == "windows" && len(child) == 2 && child[1] == ':' { + return nil, repository.ErrURIRoot + } + + parent := path.Dir(child) + if parent == "/" { + return storage.NewFileURI("/"), nil + } + + return storage.NewFileURI(parent + "/"), nil +} + +// Child creates a child URI from the given URI and component. +// +// Since: 2.0 +func (r *FileRepository) Child(u fyne.URI, component string) (fyne.URI, error) { + return storage.NewFileURI(path.Join(u.Path(), component)), nil +} + +// List returns a list of all child URIs of the given URI. +// +// Since: 2.0 +func (r *FileRepository) List(u fyne.URI) ([]fyne.URI, error) { + p := u.Path() + files, err := os.ReadDir(p) + if err != nil { + return nil, err + } + + urilist := make([]fyne.URI, len(files)) + for i, f := range files { + urilist[i] = storage.NewFileURI(path.Join(p, f.Name())) + } + + return urilist, nil +} + +// CreateListable creates a new directory at the given URI. +func (r *FileRepository) CreateListable(u fyne.URI) error { + path := u.Path() + return os.Mkdir(path, 0o755) +} + +// CanList checks if the given URI can be listed. +// +// Since: 2.0 +func (r *FileRepository) CanList(u fyne.URI) (bool, error) { + p := u.Path() + info, err := os.Stat(p) + if err != nil { + if os.IsNotExist(err) { + return false, nil + } + return false, err + } + + if !info.IsDir() { + return false, nil + } + + if runtime.GOOS == "windows" && len(p) <= 3 { + return true, nil // assume drives can be read, avoids hang if the drive is temporarily unresponsive + } + + // We know it is a directory, but we don't know if we can read it, so + // we'll just try to do so and see if we get a permission error. + f, err := os.Open(p) + if err == nil { + _, err = f.Readdir(1) + f.Close() + } + + if err != nil && err != io.EOF { + return false, err + } + + if os.IsPermission(err) { + return false, nil + } + + // it is a directory, and checking the permissions did not error out + return true, nil +} + +// Copy copies the contents of the source URI to the destination URI. +// +// Since: 2.0 +func (r *FileRepository) Copy(source, destination fyne.URI) error { + err := fastCopy(destination.Path(), source.Path()) + if err == nil { + return nil + } + + return repository.GenericCopy(source, destination) +} + +// Move moves the contents of the source URI to the destination URI. +// +// Since: 2.0 +func (r *FileRepository) Move(source, destination fyne.URI) error { + err := os.Rename(source.Path(), destination.Path()) + if err == nil { + return nil + } + + return repository.GenericMove(source, destination) +} + +func copyFile(dst, src string) error { + srcFile, err := os.Open(src) + if err != nil { + return err + } + defer srcFile.Close() + + dstFile, err := os.Create(dst) + if err != nil { + return err + } + defer dstFile.Close() + + _, err = io.Copy(dstFile, srcFile) + return err +} + +func fastCopy(dst, src string) error { + srcInfo, err := os.Stat(src) + if err != nil { + return err + } + + if !srcInfo.IsDir() { + return copyFile(dst, src) + } + + err = os.MkdirAll(dst, srcInfo.Mode()) + if err != nil { + return err + } + + return filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + rel, err := filepath.Rel(src, path) + if err != nil { + return err + } + + dstPath := filepath.Join(dst, rel) + if d.IsDir() { + info, err := d.Info() + if err != nil { + return err + } + return os.MkdirAll(dstPath, info.Mode()) + } + + return copyFile(dstPath, path) + }) +} + +func openFile(uri fyne.URI, write bool, truncate bool) (*file, error) { + path := uri.Path() + var f *os.File + var err error + if write { + if truncate { + f, err = os.Create(path) // If it exists this will truncate which is what we wanted + } else { + f, err = os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o666) + } + } else { + f, err = os.Open(path) + } + return &file{File: f, uri: uri}, err +} diff --git a/vendor/fyne.io/fyne/v2/internal/repository/http.go b/vendor/fyne.io/fyne/v2/internal/repository/http.go new file mode 100644 index 0000000..fb8fd5a --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/repository/http.go @@ -0,0 +1,99 @@ +package repository + +import ( + "errors" + "net/http" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/storage/repository" +) + +var _ repository.Repository = (*HTTPRepository)(nil) + +type remoteFile struct { + *http.Response + uri fyne.URI +} + +func (f *remoteFile) Close() error { + if f.Response == nil { + return nil + } + return f.Response.Body.Close() +} + +func (f *remoteFile) Read(p []byte) (int, error) { + if f.Response == nil { + return 0, nil + } + return f.Response.Body.Read(p) +} + +func (f *remoteFile) URI() fyne.URI { + return f.uri +} + +// HTTPRepository implements a proxy for interacting with remote resources +// using golang's net/http library. +// +// This repository is suitable to handle the http:// and https:// scheme. +// +// Since: 2.1 +type HTTPRepository struct{} + +// NewHTTPRepository creates a new HTTPRepository instance. +// The caller needs to call repository.Register() with the result of this function. +// +// Since: 2.1 +func NewHTTPRepository() *HTTPRepository { + return &HTTPRepository{} +} + +// Exists checks whether the resource at u returns a +// non "404 NOT FOUND" response header. +// +// Since: 2.1 +func (r *HTTPRepository) Exists(u fyne.URI) (bool, error) { + resp, err := http.Head(u.String()) + if err != nil { + return false, err + } + if resp.StatusCode == http.StatusNotFound { + return false, nil + } + + return true, nil +} + +// Reader provides a interface for reading the body of the response received +// from the request to u. +// +// Since: 2.1 +func (r *HTTPRepository) Reader(u fyne.URI) (fyne.URIReadCloser, error) { + resp, err := http.Get(u.String()) + return &remoteFile{Response: resp, uri: u}, err +} + +// CanRead makes a HEAD HTTP request to analyse the headers received +// from the remote server. +// Any response status code apart from 2xx is considered to be invalid. +// +// Since: 2.1 +func (r *HTTPRepository) CanRead(u fyne.URI) (bool, error) { + resp, err := http.Head(u.String()) + if err != nil { + return false, err + } + if resp.StatusCode < http.StatusOK || resp.StatusCode > http.StatusIMUsed { + return false, errors.New("remote server did not return a successful response") + } + + return true, nil +} + +// Destroy satisfies the repository.Repository interface. +// +// Since: 2.1 +func (r *HTTPRepository) Destroy(string) { + // do nothing +} diff --git a/vendor/fyne.io/fyne/v2/internal/repository/indexdb_file_wasm.go b/vendor/fyne.io/fyne/v2/internal/repository/indexdb_file_wasm.go new file mode 100644 index 0000000..bd95212 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/repository/indexdb_file_wasm.go @@ -0,0 +1,175 @@ +//go:build wasm + +package repository + +import ( + "context" + "fmt" + "io" + "syscall/js" + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/storage" + + "github.com/hack-pad/go-indexeddb/idb" +) + +var ( + blob js.Value + uint8Array js.Value +) + +func init() { + blob = js.Global().Get("Blob") + uint8Array = js.Global().Get("Uint8Array") +} + +var ( + _ fyne.URIReadCloser = (*idbfile)(nil) + _ fyne.URIWriteCloser = (*idbfile)(nil) +) + +type idbfile struct { + db *idb.Database + path string + parent string + isDir bool + truncate bool + isTruncated bool + parts []any + add bool + isAdding bool +} + +func (f *idbfile) Close() error { + return nil +} + +func (f *idbfile) URI() fyne.URI { + u, _ := storage.ParseURI(idbfileSchemePrefix + f.path) + return u +} + +func (f *idbfile) rwstore(name string) (*idb.ObjectStore, error) { + txn, err := f.db.Transaction(idb.TransactionReadWrite, name) + if err != nil { + return nil, err + } + store, err := txn.ObjectStore(name) + if err != nil { + return nil, err + } + return store, nil +} + +func (f *idbfile) Write(data []byte) (int, error) { + p := f.path + ctx := context.Background() + + m := map[string]any{ + "parent": f.parent, + "size": 0, + "ctime": 0, + "mtime": 0, + } + + if f.truncate && !f.isTruncated { + store, err := f.rwstore("data") + if err != nil { + return 0, err + } + delreq, err := store.Delete(js.ValueOf(p)) + if err != nil { + return 0, err + } + if err := delreq.Await(ctx); err != nil { + return 0, err + } + f.isTruncated = true + + m["ctime"] = time.Now().UnixMilli() + m["mtime"] = m["ctime"] + } + + if f.add && !f.isAdding { + b, err := get(f.db, "data", f.path) + if err != nil { + return 0, err + } + + f.parts = []any{getBytes(b)} + f.isAdding = true + + meta, err := get(f.db, "meta", f.path) + if err != nil { + return 0, err + } + + m["ctime"] = meta.Get("ctime").Int() + m["mtime"] = time.Now().UnixMilli() + } + + a := uint8Array.New(len(data)) + n := js.CopyBytesToJS(a, data) + f.parts = append(f.parts, a) + b := blob.New(js.ValueOf(f.parts)) + + m["size"] = b.Get("size").Int() + + metastore, err := f.rwstore("meta") + if err != nil { + return 0, err + } + + metareq, err := metastore.PutKey(js.ValueOf(p), js.ValueOf(m)) + if err != nil { + return 0, err + } + + store, err := f.rwstore("data") + if err != nil { + return 0, err + } + req, err := store.PutKey(js.ValueOf(p), b) + if err != nil { + return 0, err + } + + if _, err := metareq.Await(ctx); err != nil { + return 0, err + } + + _, err = req.Await(ctx) + return n, err +} + +func getBytes(b js.Value) js.Value { + outch := make(chan js.Value) + send := js.FuncOf(func(this js.Value, args []js.Value) any { + outch <- args[0] + return nil + }) + defer send.Release() + + b.Call("arrayBuffer").Call("then", send) + buf := <-outch + return uint8Array.New(buf) +} + +func (f *idbfile) Read(data []byte) (int, error) { + b, err := get(f.db, "data", f.path) + if err != nil { + return 0, err + } + + if b.IsUndefined() { + return 0, fmt.Errorf("idbfile undefined") + } + + if !b.InstanceOf(blob) { + return 0, fmt.Errorf("returned object not of type blob") + } + + return js.CopyBytesToGo(data, getBytes(b)), io.EOF +} diff --git a/vendor/fyne.io/fyne/v2/internal/repository/indexdb_wasm.go b/vendor/fyne.io/fyne/v2/internal/repository/indexdb_wasm.go new file mode 100644 index 0000000..3773c0b --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/repository/indexdb_wasm.go @@ -0,0 +1,339 @@ +//go:build wasm + +package repository + +import ( + "context" + "syscall/js" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/storage" + "fyne.io/fyne/v2/storage/repository" + + "github.com/hack-pad/go-indexeddb/idb" +) + +// fileSchemePrefix is used for when we need a hard-coded version of "idbfile://" +// for string processing +const idbfileSchemePrefix string = "idbfile://" + +var ( + _ repository.Repository = (*IndexDBRepository)(nil) + _ repository.WritableRepository = (*IndexDBRepository)(nil) + _ repository.AppendableRepository = (*IndexDBRepository)(nil) + _ repository.HierarchicalRepository = (*IndexDBRepository)(nil) + _ repository.ListableRepository = (*IndexDBRepository)(nil) + _ repository.MovableRepository = (*IndexDBRepository)(nil) + _ repository.CopyableRepository = (*IndexDBRepository)(nil) +) + +type IndexDBRepository struct { + db *idb.Database +} + +func NewIndexDBRepository() (*IndexDBRepository, error) { + ctx := context.Background() + req, err := idb.Global().Open(ctx, "files", 1, func(db *idb.Database, oldVer, newVer uint) error { + metastore, err := db.CreateObjectStore("meta", idb.ObjectStoreOptions{}) + if err != nil { + return err + } + _, err = metastore.CreateIndex("path", js.ValueOf(""), idb.IndexOptions{Unique: true}) + if err != nil { + return err + } + _, err = metastore.CreateIndex("parent", js.ValueOf("parent"), idb.IndexOptions{}) + if err != nil { + return err + } + + datastore, err := db.CreateObjectStore("data", idb.ObjectStoreOptions{}) + if err != nil { + return err + } + _, err = datastore.CreateIndex("path", js.ValueOf(""), idb.IndexOptions{Unique: true}) + return err + }) + db, err := req.Await(ctx) + if err != nil { + return nil, err + } + + if err := mkdir(db, "/", ""); err != nil { + return nil, err + } + + return &IndexDBRepository{db: db}, nil +} + +func (r *IndexDBRepository) Exists(u fyne.URI) (bool, error) { + p := u.Path() + ctx := context.Background() + txn, err := r.db.Transaction(idb.TransactionReadOnly, "meta") + if err != nil { + return false, err + } + store, err := txn.ObjectStore("meta") + if err != nil { + return false, err + } + req, err := store.CountKey(js.ValueOf(p)) + if err != nil { + return false, err + } + n, err := req.Await(ctx) + if err != nil { + return false, err + } + + return n != 0, nil +} + +func get(db *idb.Database, s, p string) (js.Value, error) { + ctx := context.Background() + + txn, err := db.Transaction(idb.TransactionReadOnly, s) + if err != nil { + return js.Undefined(), err + } + store, err := txn.ObjectStore(s) + if err != nil { + return js.Undefined(), err + } + req, err := store.Get(js.ValueOf(p)) + if err != nil { + return js.Undefined(), err + } + + v, err := req.Await(ctx) + if err != nil { + return js.Undefined(), err + } + + return v, nil +} + +func (r *IndexDBRepository) CanList(u fyne.URI) (bool, error) { + p := u.Path() + + v, err := get(r.db, "meta", p) + if err != nil { + return false, err + } + + if v.IsUndefined() { + return false, nil + } + + isDir := v.Get("isDir") + if isDir.IsUndefined() { + return false, nil + } + + return isDir.Bool(), nil +} + +func mkdir(db *idb.Database, dir, parent string) error { + ctx := context.Background() + txn, err := db.Transaction(idb.TransactionReadWrite, "meta") + if err != nil { + return err + } + + store, err := txn.ObjectStore("meta") + if err != nil { + return err + } + + f := map[string]any{ + "isDir": true, + "parent": parent, + } + req, err := store.PutKey(js.ValueOf(dir), js.ValueOf(f)) + if err != nil { + return err + } + + _, err = req.Await(ctx) + return err +} + +func (r *IndexDBRepository) CreateListable(u fyne.URI) error { + pu, err := storage.Parent(u) + if err != nil { + return err + } + return mkdir(r.db, u.Path(), pu.Path()) +} + +func (r *IndexDBRepository) CanRead(u fyne.URI) (bool, error) { + p := u.Path() + + v, err := get(r.db, "meta", p) + if err != nil { + return false, err + } + + if v.IsUndefined() { + return false, nil + } + + return true, nil +} + +func (r *IndexDBRepository) Destroy(scheme string) { + // do nothing +} + +func (r *IndexDBRepository) List(u fyne.URI) ([]fyne.URI, error) { + p := u.Path() + ctx := context.Background() + txn, err := r.db.Transaction(idb.TransactionReadOnly, "meta") + if err != nil { + return nil, err + } + + store, err := txn.ObjectStore("meta") + if err != nil { + return nil, err + } + + idx, err := store.Index("parent") + if err != nil { + return nil, err + } + + creq, err := idx.OpenCursorKey(js.ValueOf(p), idb.CursorNext) + if err != nil { + return nil, err + } + + paths := []string{} + if err := creq.Iter(ctx, func(cwv *idb.CursorWithValue) error { + k, err := cwv.PrimaryKey() + if err != nil { + return err + } + paths = append(paths, idbfileSchemePrefix+k.String()) + return nil + }); err != nil { + return nil, err + } + + us := make([]fyne.URI, len(paths)) + for n, path := range paths { + us[n], err = storage.ParseURI(path) + if err != nil { + return nil, err + } + } + return us, nil +} + +func (r *IndexDBRepository) CanWrite(u fyne.URI) (bool, error) { + p := u.Path() + v, err := get(r.db, "meta", p) + if err != nil { + return false, err + } + + if v.IsUndefined() { + return true, nil + } + + isDir := v.Get("isDir") + if isDir.IsUndefined() { + return true, nil + } + + return !isDir.Bool(), nil +} + +func (r *IndexDBRepository) Delete(u fyne.URI) error { + p := u.Path() + ctx := context.Background() + txn, err := r.db.Transaction(idb.TransactionReadWrite, "meta", "data") + if err != nil { + return err + } + + metastore, err := txn.ObjectStore("meta") + if err != nil { + return err + } + + metareq, err := metastore.Delete(js.ValueOf(p)) + if err != nil { + return err + } + if err := metareq.Await(ctx); err != nil { + return err + } + + datastore, err := txn.ObjectStore("data") + if err != nil { + return err + } + datareq, err := datastore.Delete(js.ValueOf(p)) + if err != nil { + return err + } + return datareq.Await(ctx) +} + +func (r *IndexDBRepository) Reader(u fyne.URI) (fyne.URIReadCloser, error) { + pu, err := storage.Parent(u) + if err != nil { + return nil, err + } + + return &idbfile{ + db: r.db, + path: u.Path(), + parent: pu.Path(), + }, nil +} + +func (r *IndexDBRepository) Writer(u fyne.URI) (fyne.URIWriteCloser, error) { + pu, err := storage.Parent(u) + if err != nil { + return nil, err + } + + return &idbfile{ + db: r.db, + path: u.Path(), + parent: pu.Path(), + truncate: true, + }, nil +} + +func (r *IndexDBRepository) Appender(u fyne.URI) (fyne.URIWriteCloser, error) { + pu, err := storage.Parent(u) + if err != nil { + return nil, err + } + + return &idbfile{ + db: r.db, + path: u.Path(), + parent: pu.Path(), + add: true, + }, nil +} + +func (r *IndexDBRepository) Copy(src, dst fyne.URI) error { + return repository.GenericCopy(src, dst) +} + +func (r *IndexDBRepository) Move(source, destination fyne.URI) error { + return repository.GenericMove(source, destination) +} + +func (r *IndexDBRepository) Child(u fyne.URI, component string) (fyne.URI, error) { + return repository.GenericChild(u, component) +} + +func (r *IndexDBRepository) Parent(u fyne.URI) (fyne.URI, error) { + return repository.GenericParent(u) +} diff --git a/vendor/fyne.io/fyne/v2/internal/repository/memory.go b/vendor/fyne.io/fyne/v2/internal/repository/memory.go new file mode 100644 index 0000000..5a0aaa5 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/repository/memory.go @@ -0,0 +1,346 @@ +package repository + +import ( + "fmt" + "io" + "strings" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/storage" + "fyne.io/fyne/v2/storage/repository" +) + +// declare conformance to interfaces +var ( + _ io.ReadCloser = (*nodeReaderWriter)(nil) + _ io.WriteCloser = (*nodeReaderWriter)(nil) + _ fyne.URIReadCloser = (*nodeReaderWriter)(nil) + _ fyne.URIWriteCloser = (*nodeReaderWriter)(nil) +) + +// declare conformance with repository types +var ( + _ repository.Repository = (*InMemoryRepository)(nil) + _ repository.WritableRepository = (*InMemoryRepository)(nil) + _ repository.AppendableRepository = (*InMemoryRepository)(nil) + _ repository.HierarchicalRepository = (*InMemoryRepository)(nil) + _ repository.CopyableRepository = (*InMemoryRepository)(nil) + _ repository.MovableRepository = (*InMemoryRepository)(nil) + _ repository.ListableRepository = (*InMemoryRepository)(nil) +) + +// nodeReaderWriter allows reading or writing to elements in a InMemoryRepository +type nodeReaderWriter struct { + path string + repo *InMemoryRepository + writing bool + readCursor int + writeCursor int +} + +// InMemoryRepository implements an in-memory version of the +// repository.Repository type. It is useful for writing test cases, and may +// also be of use as a template for people wanting to implement their own +// "virtual repository". In future, we may consider moving this into the public +// API. +// +// Because of its design, this repository has several quirks: +// +// * The Parent() of a path that exists does not necessarily exist +// +// - Listing takes O(number of extant paths in the repository), rather than +// O(number of children of path being listed). +// +// This repository is not designed to be particularly fast or robust, but +// rather to be simple and easy to read. If you need performance, look +// elsewhere. +// +// Since: 2.0 +type InMemoryRepository struct { + // Data is exposed to allow tests to directly insert their own data + // without having to go through the API + Data map[string][]byte + + scheme string +} + +// Read reads data from the repository into the provided buffer. +func (n *nodeReaderWriter) Read(p []byte) (int, error) { + // first make sure the requested path actually exists + data, ok := n.repo.Data[n.path] + if !ok { + return 0, fmt.Errorf("path '%s' not present in InMemoryRepository", n.path) + } + + // copy it into p - we maintain counts since len(data) may be smaller + // than len(p) + count := 0 + j := 0 // index into p + for ; (j < len(p)) && (n.readCursor < len(data)); n.readCursor++ { + p[j] = data[n.readCursor] + count++ + j++ + } + + // generate EOF if needed + var err error = nil + if n.readCursor >= len(data) { + err = io.EOF + } + + return count, err +} + +// Close closes the reader and writer. +func (n *nodeReaderWriter) Close() error { + n.readCursor = 0 + n.writeCursor = 0 + n.writing = false + return nil +} + +// Write writes data to the repository. +// +// This implementation automatically creates the path n.path if it does not +// exist. If it does exist, it is overwritten. +func (n *nodeReaderWriter) Write(p []byte) (int, error) { + // overwrite the file if we haven't already started writing to it + if !n.writing { + n.repo.Data[n.path] = make([]byte, 0, len(p)) + n.writing = true + } + + // copy the data into the node buffer + for start := n.writeCursor; n.writeCursor < start+len(p); n.writeCursor++ { + // extend the file if needed + if len(n.repo.Data) < n.writeCursor+len(p) { + n.repo.Data[n.path] = append(n.repo.Data[n.path], 0) + } + n.repo.Data[n.path][n.writeCursor] = p[n.writeCursor-start] + } + + return len(p), nil +} + +// URI returns the URI of the node. +func (n *nodeReaderWriter) URI() fyne.URI { + // discarding the error because this should never fail + u, _ := storage.ParseURI(n.repo.scheme + "://" + n.path) + return u +} + +// NewInMemoryRepository creates a new InMemoryRepository instance. It must be +// given the scheme it is registered for. The caller needs to call +// repository.Register() on the result of this function. +// +// Since: 2.0 +func NewInMemoryRepository(scheme string) *InMemoryRepository { + return &InMemoryRepository{ + Data: make(map[string][]byte), + scheme: scheme, + } +} + +// Exists checks if the given URI exists. +// +// Since: 2.0 +func (m *InMemoryRepository) Exists(u fyne.URI) (bool, error) { + path := u.Path() + if path == "" { + return false, fmt.Errorf("invalid path '%s'", path) + } + + _, ok := m.Data[path] + return ok, nil +} + +// Reader reads the contents of the given URI. +// +// Since: 2.0 +func (m *InMemoryRepository) Reader(u fyne.URI) (fyne.URIReadCloser, error) { + path := u.Path() + + if path == "" { + return nil, fmt.Errorf("invalid path '%s'", path) + } + + _, ok := m.Data[path] + if !ok { + return nil, fmt.Errorf("no such path '%s' in InMemoryRepository", path) + } + + return &nodeReaderWriter{path: path, repo: m}, nil +} + +// CanRead checks if the given URI can be read. +// +// Since: 2.0 +func (m *InMemoryRepository) CanRead(u fyne.URI) (bool, error) { + path := u.Path() + if path == "" { + return false, fmt.Errorf("invalid path '%s'", path) + } + + _, ok := m.Data[path] + return ok, nil +} + +// Destroy tears down the InMemoryRepository. +func (m *InMemoryRepository) Destroy(scheme string) { + // do nothing +} + +// Writer writes to the given URI. +// +// Since: 2.0 +func (m *InMemoryRepository) Writer(u fyne.URI) (fyne.URIWriteCloser, error) { + path := u.Path() + if path == "" { + return nil, fmt.Errorf("invalid path '%s'", path) + } + + return &nodeReaderWriter{path: path, repo: m}, nil +} + +// Appender returns a writer that appends to the given URI. +// +// Since: 2.6 +func (m *InMemoryRepository) Appender(u fyne.URI) (fyne.URIWriteCloser, error) { + path := u.Path() + if path == "" { + return nil, fmt.Errorf("invalid path '%s'", path) + } + + return &nodeReaderWriter{path: path, repo: m, writing: true, writeCursor: len(m.Data[path])}, nil +} + +// CanWrite checks if the given URI can be written to. +// +// Since: 2.0 +func (m *InMemoryRepository) CanWrite(u fyne.URI) (bool, error) { + if p := u.Path(); p == "" { + return false, fmt.Errorf("invalid path '%s'", p) + } + + return true, nil +} + +// Delete deletes the given URI. +// +// Since: 2.0 +func (m *InMemoryRepository) Delete(u fyne.URI) error { + path := u.Path() + _, ok := m.Data[path] + if ok { + delete(m.Data, path) + } + + return nil +} + +// Parent returns the parent URI of the given URI. +// +// Since: 2.0 +func (m *InMemoryRepository) Parent(u fyne.URI) (fyne.URI, error) { + return repository.GenericParent(u) +} + +// Child returns the child URI created from the given URI and component. +// +// Since: 2.0 +func (m *InMemoryRepository) Child(u fyne.URI, component string) (fyne.URI, error) { + return repository.GenericChild(u, component) +} + +// Copy copies the source URI to the destination URI. +// +// Since: 2.0 +func (m *InMemoryRepository) Copy(source, destination fyne.URI) error { + return repository.GenericCopy(source, destination) +} + +// Move moves the contents of the source URI to the destination. +// +// Since: 2.0 +func (m *InMemoryRepository) Move(source, destination fyne.URI) error { + return repository.GenericMove(source, destination) +} + +// CanList checks if the given URI can be listed. +// +// Since: 2.0 +func (m *InMemoryRepository) CanList(u fyne.URI) (bool, error) { + path := u.Path() + exist, err := m.Exists(u) + if err != nil || !exist { + return false, err + } + + if path == "" || path[len(path)-1] == '/' { + return true, nil + } + + children, err := m.List(u) + return len(children) > 0, err +} + +// List returns a list of URIs that are children of the given URI. +// +// Since: 2.0 +func (m *InMemoryRepository) List(u fyne.URI) ([]fyne.URI, error) { + // Get the prefix, and make sure it ends with a path separator so that + // HasPrefix() will only find things that are children of it - this + // solves the edge case where you have say '/foo/bar' and + // '/foo/barbaz'. + prefix := u.Path() + + if len(prefix) > 0 && prefix[len(prefix)-1] != '/' { + prefix = prefix + "/" + } + + prefixSplit := strings.Split(prefix, "/") + prefixSplitLen := len(prefixSplit) + + // Now we can simply loop over all the paths and find the ones with an + // appropriate prefix, then eliminate those with too many path + // components. + listing := []fyne.URI{} + for p := range m.Data { + // We are going to compare ncomp with the number of elements in + // prefixSplit, which is guaranteed to have a trailing slash, + // so we want to also make pSplit be counted in ncomp like it + // does not have one. + pSplit := strings.Split(p, "/") + ncomp := len(pSplit) + if len(p) > 0 && p[len(p)-1] == '/' { + ncomp-- + } + + if strings.HasPrefix(p, prefix) && ncomp == prefixSplitLen { + uri, err := storage.ParseURI(m.scheme + "://" + p) + if err != nil { + return nil, err + } + + listing = append(listing, uri) + } + } + + return listing, nil +} + +// CreateListable makes the given URI a listable URI. +// +// Since: 2.0 +func (m *InMemoryRepository) CreateListable(u fyne.URI) error { + ex, err := m.Exists(u) + if err != nil { + return err + } + path := u.Path() + if ex { + return fmt.Errorf("cannot create '%s' as a listable path because it already exists", path) + } + m.Data[path] = []byte{} + return nil +} diff --git a/vendor/fyne.io/fyne/v2/internal/repository/mime/mime.go b/vendor/fyne.io/fyne/v2/internal/repository/mime/mime.go new file mode 100644 index 0000000..e3281d6 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/repository/mime/mime.go @@ -0,0 +1,13 @@ +package mime + +import "strings" + +// Split spits the mimetype into its main type and subtype. +func Split(mimeTypeFull string) (mimeType, mimeSubType string) { + mimeType, mimeSubType, ok := strings.Cut(mimeTypeFull, "/") + if !ok || mimeSubType == "" { + return "", "" + } + + return mimeType, mimeSubType +} diff --git a/vendor/fyne.io/fyne/v2/internal/scale/scale.go b/vendor/fyne.io/fyne/v2/internal/scale/scale.go new file mode 100644 index 0000000..fb98713 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/scale/scale.go @@ -0,0 +1,43 @@ +package scale + +import ( + "math" + + "fyne.io/fyne/v2" +) + +// ToScreenCoordinate converts a fyne coordinate in the given canvas to a screen coordinate +func ToScreenCoordinate(c fyne.Canvas, v float32) int { + return int(math.Ceil(float64(v * c.Scale()))) +} + +// ToFyneCoordinate converts a screen coordinate for a given canvas to a fyne coordinate +func ToFyneCoordinate(c fyne.Canvas, v int) float32 { + switch c.Scale() { + case 0.0: + panic("Incorrect scale most likely not set.") + case 1.0: + return float32(v) + default: + return float32(v) / c.Scale() + } +} + +// ToFyneSize returns the scaled size of an object based on pixel coordinates, typically for images. +// This method will attempt to find the canvas for an object to get its scale. +// In the event that this fails it will assume a 1:1 mapping (scale=1 or low DPI display). +func ToFyneSize(obj fyne.CanvasObject, width, height int) fyne.Size { + app := fyne.CurrentApp() + if app == nil { + return fyne.NewSize(float32(width), float32(height)) // can occur if called before app.New + } + driver := app.Driver() + if driver == nil { + return fyne.NewSize(float32(width), float32(height)) + } + c := driver.CanvasForObject(obj) + if c == nil { + return fyne.NewSize(float32(width), float32(height)) // this will happen a lot during init + } + return fyne.NewSize(ToFyneCoordinate(c, width), ToFyneCoordinate(c, height)) +} diff --git a/vendor/fyne.io/fyne/v2/internal/svg/svg.go b/vendor/fyne.io/fyne/v2/internal/svg/svg.go new file mode 100644 index 0000000..6d543d1 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/svg/svg.go @@ -0,0 +1,330 @@ +package svg + +import ( + "bytes" + "encoding/hex" + "encoding/xml" + "errors" + "fmt" + "image" + "image/color" + "io" + "path/filepath" + "strconv" + "strings" + + "github.com/fyne-io/oksvg" + "github.com/srwiley/rasterx" + + "fyne.io/fyne/v2" + col "fyne.io/fyne/v2/internal/color" +) + +// Colorize creates a new SVG from a given one by replacing all fill colors by the given color. +func Colorize(src []byte, clr color.Color) ([]byte, error) { + rdr := bytes.NewReader(src) + s, err := svgFromXML(rdr) + if err != nil { + return src, fmt.Errorf("could not load SVG, falling back to static content: %v", err) + } + if err := s.replaceFillColor(clr); err != nil { + return src, fmt.Errorf("could not replace fill color, falling back to static content: %v", err) + } + colorized, err := xml.Marshal(s) + if err != nil { + return src, fmt.Errorf("could not marshal svg, falling back to static content: %v", err) + } + return colorized, nil +} + +type Decoder struct { + icon *oksvg.SvgIcon +} + +type Config struct { + Width int + Height int + Aspect float32 +} + +func NewDecoder(stream io.Reader) (*Decoder, error) { + icon, err := oksvg.ReadIconStream(stream) + if err != nil { + return nil, err + } + + return &Decoder{ + icon: icon, + }, nil +} + +func (d *Decoder) Config() Config { + return Config{ + int(d.icon.ViewBox.W), + int(d.icon.ViewBox.H), + float32(d.icon.ViewBox.W / d.icon.ViewBox.H), + } +} + +func (d *Decoder) Draw(width, height int) (*image.NRGBA, error) { + config := d.Config() + + viewAspect := float32(width) / float32(height) + imgW, imgH := width, height + if viewAspect > config.Aspect { + imgW = int(float32(height) * config.Aspect) + } else if viewAspect < config.Aspect { + imgH = int(float32(width) / config.Aspect) + } + + x, y := svgOffset(d.icon, imgW, imgH) + d.icon.SetTarget(x, y, float64(imgW), float64(imgH)) + + img := image.NewNRGBA(image.Rect(0, 0, imgW, imgH)) + scanner := rasterx.NewScannerGV(config.Width, config.Height, img, img.Bounds()) + raster := rasterx.NewDasher(width, height, scanner) + + err := drawSVGSafely(d.icon, raster) + if err != nil { + err = fmt.Errorf("SVG render error: %w", err) + return nil, err + } + return img, nil +} + +func IsFileSVG(path string) bool { + return strings.EqualFold(filepath.Ext(path), ".svg") +} + +// IsResourceSVG checks if the resource is an SVG or not. +func IsResourceSVG(res fyne.Resource) bool { + if IsFileSVG(res.Name()) { + return true + } + + if len(res.Content()) < 5 { + return false + } + + switch strings.ToLower(string(res.Content()[:5])) { + case " 4 || delta < -4 { + return false + } + mismatches++ + } + + // Allow up to 1% of pixels to mismatch. + return mismatches == 0 || mismatches < len(a)/100 +} + +// NewCheckedImage returns a new black/white checked image with the specified size +// and the specified amount of horizontal and vertical tiles. +func NewCheckedImage(w, h, hTiles, vTiles int) image.Image { + img := image.NewNRGBA(image.Rect(0, 0, w, h)) + colors := []color.Color{color.White, color.Black} + tileWidth := float64(w) / float64(hTiles) + tileHeight := float64(h) / float64(vTiles) + for y := 0; y < h; y++ { + yTile := int(math.Floor(float64(y) / tileHeight)) + for x := 0; x < w; x++ { + xTile := int(math.Floor(float64(x) / tileWidth)) + img.Set(x, y, colors[(xTile+yTile)%2]) + } + } + return img +} + +func writeImage(path string, img image.Image) error { + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return err + } + f, err := os.Create(path) + if err != nil { + return err + } + if err = png.Encode(f, img); err != nil { + f.Close() + return err + } + return f.Close() +} diff --git a/vendor/fyne.io/fyne/v2/internal/test/util_helper.go b/vendor/fyne.io/fyne/v2/internal/test/util_helper.go new file mode 100644 index 0000000..44f8026 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/test/util_helper.go @@ -0,0 +1,73 @@ +//go:build !tamago && !noos + +package test + +import ( + "fmt" + "image" + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// AssertImageMatches asserts that the given image is the same as the one stored in the master file. +// The master filename is relative to the `testdata` directory which is relative to the test. +// The test `t` fails if the given image is not equal to the loaded master image. +// In this case the given image is written into a file in `testdata/failed/` (relative to the test). +// This path is also reported, thus the file can be used as new master. +func AssertImageMatches(t *testing.T, masterFilename string, img image.Image, msgAndArgs ...any) bool { + wd, err := os.Getwd() + require.NoError(t, err) + masterPath := filepath.Join(wd, "testdata", masterFilename) + failedPath := filepath.Join(wd, "testdata/failed", masterFilename) + _, err = os.Stat(masterPath) + if os.IsNotExist(err) { + require.NoError(t, writeImage(failedPath, img)) + t.Errorf("Master not found at %s. Image written to %s might be used as master.", masterPath, failedPath) + return false + } + + file, err := os.Open(masterPath) + require.NoError(t, err) + defer file.Close() + raw, _, err := image.Decode(file) + require.NoError(t, err) + + masterPix := pixelsForImage(t, raw) // let's just compare the pixels directly + capturePix := pixelsForImage(t, img) + + // On darwin/arm64, there are slight differences in the rendering. + // Use a slower, more lenient comparison. If that fails, + // fall back to the strict comparison for a more detailed error message. + if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" && pixCloseEnough(masterPix, capturePix) { + return true + } + + var msg string + if len(msgAndArgs) > 0 { + msg = fmt.Sprintf(msgAndArgs[0].(string)+"\n", msgAndArgs[1:]...) + } + if !assert.Equal(t, masterPix, capturePix, "%sImage did not match master. Actual image written to file://%s.", msg, failedPath) { + require.NoError(t, writeImage(failedPath, img)) + return false + } + return true +} + +func pixelsForImage(t *testing.T, img image.Image) []uint8 { + var pix []uint8 + if data, ok := img.(*image.RGBA); ok { + pix = data.Pix + } else if data, ok := img.(*image.NRGBA); ok { + pix = data.Pix + } + if pix == nil { + t.Error("Master image is unsupported type") + } + + return pix +} diff --git a/vendor/fyne.io/fyne/v2/internal/theme/feature.go b/vendor/fyne.io/fyne/v2/internal/theme/feature.go new file mode 100644 index 0000000..dc3e479 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/theme/feature.go @@ -0,0 +1,29 @@ +package theme + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/cache" +) + +type FeatureName string + +const FeatureNameDeviceIsMobile = FeatureName("deviceIsMobile") + +// FeatureTheme defines the method to look up features that we use internally to apply functional +// differences through a theme override. +type FeatureTheme interface { + Feature(FeatureName) any +} + +// FeatureForWidget looks up the specified feature flag for the requested widget using the current theme. +// This is for internal purposes and will do nothing if the theme has not been overridden with the +// ThemeOverride container. +func FeatureForWidget(name FeatureName, w fyne.Widget) any { + if custom := cache.WidgetTheme(w); custom != nil { + if f, ok := custom.(FeatureTheme); ok { + return f.Feature(name) + } + } + + return nil +} diff --git a/vendor/fyne.io/fyne/v2/internal/theme/render.go b/vendor/fyne.io/fyne/v2/internal/theme/render.go new file mode 100644 index 0000000..55773a1 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/theme/render.go @@ -0,0 +1,30 @@ +package theme + +import ( + "fyne.io/fyne/v2" +) + +var themeStack []fyne.Theme + +// CurrentlyRenderingWithFallback returns the theme that is currently being used during rendering or layout +// calculations. If there is no override in effect then the fallback is returned. +func CurrentlyRenderingWithFallback(f fyne.Theme) fyne.Theme { + if len(themeStack) == 0 { + return f + } + + return themeStack[len(themeStack)-1] +} + +// PushRenderingTheme is used by the ThemeOverride container to stack the current theme during rendering +// and calculations. +func PushRenderingTheme(th fyne.Theme) { + themeStack = append(themeStack, th) +} + +// PopRenderingTheme is used by the ThemeOverride container to remove an overridden theme during rendering +// and calculations. +func PopRenderingTheme() { + themeStack[len(themeStack)-1] = nil + themeStack = themeStack[:len(themeStack)-1] +} diff --git a/vendor/fyne.io/fyne/v2/internal/theme/theme.go b/vendor/fyne.io/fyne/v2/internal/theme/theme.go new file mode 100644 index 0000000..8e122f1 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/theme/theme.go @@ -0,0 +1,93 @@ +package theme + +import ( + "image/color" + + "fyne.io/fyne/v2" +) + +// Primary color names. +const ( + ColorBlue = "blue" + ColorBrown = "brown" + ColorGray = "gray" + ColorGreen = "green" + ColorOrange = "orange" + ColorPurple = "purple" + ColorRed = "red" + ColorYellow = "yellow" +) + +// Theme variants; the public available ones are defined in /theme/theme.go. +const ( + VariantDark fyne.ThemeVariant = iota + VariantLight + VariantNameUserPreference // locally used in builtinTheme for backward compatibility +) + +var ( + colorLightOnPrimaryBlue = color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff} + colorLightOnPrimaryBrown = color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff} + colorLightOnPrimaryGray = color.NRGBA{R: 0x17, G: 0x17, B: 0x18, A: 0xff} + colorLightOnPrimaryGreen = color.NRGBA{R: 0x17, G: 0x17, B: 0x18, A: 0xff} + colorLightOnPrimaryOrange = color.NRGBA{R: 0x17, G: 0x17, B: 0x18, A: 0xff} + colorLightOnPrimaryPurple = color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff} + colorLightOnPrimaryRed = color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff} + colorLightOnPrimaryYellow = color.NRGBA{R: 0x17, G: 0x17, B: 0x18, A: 0xff} + colorLightPrimaryBlue = color.NRGBA{R: 0x29, G: 0x6f, B: 0xf6, A: 0xff} + colorLightPrimaryBrown = color.NRGBA{R: 0x79, G: 0x55, B: 0x48, A: 0xff} + colorLightPrimaryGray = color.NRGBA{R: 0x9e, G: 0x9e, B: 0x9e, A: 0xff} + colorLightPrimaryGreen = color.NRGBA{R: 0x8b, G: 0xc3, B: 0x4a, A: 0xff} + colorLightPrimaryOrange = color.NRGBA{R: 0xff, G: 0x98, B: 0x00, A: 0xff} + colorLightPrimaryPurple = color.NRGBA{R: 0x9c, G: 0x27, B: 0xb0, A: 0xff} + colorLightPrimaryRed = color.NRGBA{R: 0xf4, G: 0x43, B: 0x36, A: 0xff} + colorLightPrimaryYellow = color.NRGBA{R: 0xff, G: 0xeb, B: 0x3b, A: 0xff} +) + +// ForegroundOnPrimaryColorNamed returns a theme specific color used for text and icons against the named primary color. +func ForegroundOnPrimaryColorNamed(name string) color.Color { + switch name { + case ColorRed: + return colorLightOnPrimaryRed + case ColorOrange: + return colorLightOnPrimaryOrange + case ColorYellow: + return colorLightOnPrimaryYellow + case ColorGreen: + return colorLightOnPrimaryGreen + case ColorPurple: + return colorLightOnPrimaryPurple + case ColorBrown: + return colorLightOnPrimaryBrown + case ColorGray: + return colorLightOnPrimaryGray + } + + // We return the “on” value for ColorBlue for every other value. + // There is no need to have it in the switch above. + return colorLightOnPrimaryBlue +} + +// PrimaryColorNamed returns a theme specific color value for a named primary color. +func PrimaryColorNamed(name string) color.Color { + switch name { + case ColorRed: + return colorLightPrimaryRed + case ColorOrange: + return colorLightPrimaryOrange + case ColorYellow: + return colorLightPrimaryYellow + case ColorGreen: + return colorLightPrimaryGreen + case ColorPurple: + return colorLightPrimaryPurple + case ColorBrown: + return colorLightPrimaryBrown + case ColorGray: + return colorLightPrimaryGray + } + + // We return the value for ColorBlue for every other value. + // There is no need to have it in the switch above. + return colorLightPrimaryBlue +} diff --git a/vendor/fyne.io/fyne/v2/internal/widget/base.go b/vendor/fyne.io/fyne/v2/internal/widget/base.go new file mode 100644 index 0000000..e4aeef0 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/widget/base.go @@ -0,0 +1,143 @@ +package widget + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/internal/cache" +) + +// Base provides a helper that handles basic widget behaviours. +type Base struct { + hidden bool + position fyne.Position + size fyne.Size + impl fyne.Widget +} + +// ExtendBaseWidget is used by an extending widget to make use of BaseWidget functionality. +func (w *Base) ExtendBaseWidget(wid fyne.Widget) { + impl := w.super() + if impl != nil { + return + } + + w.impl = wid +} + +// Size gets the current size of this widget. +func (w *Base) Size() fyne.Size { + return w.size +} + +// Resize sets a new size for a widget. +// Note this should not be used if the widget is being managed by a Layout within a Container. +func (w *Base) Resize(size fyne.Size) { + if size == w.Size() { + return + } + + w.size = size + + impl := w.super() + if impl == nil { + return + } + cache.Renderer(impl).Layout(size) +} + +// Position gets the current position of this widget, relative to its parent. +func (w *Base) Position() fyne.Position { + return w.position +} + +// Move the widget to a new position, relative to its parent. +// Note this should not be used if the widget is being managed by a Layout within a Container. +func (w *Base) Move(pos fyne.Position) { + if w.Position() == pos { + return + } + + w.position = pos + + Repaint(w.super()) +} + +// MinSize for the widget - it should never be resized below this value. +func (w *Base) MinSize() fyne.Size { + impl := w.super() + + r := cache.Renderer(impl) + if r == nil { + return fyne.NewSize(0, 0) + } + + return r.MinSize() +} + +// Visible returns whether or not this widget should be visible. +// Note that this may not mean it is currently visible if a parent has been hidden. +func (w *Base) Visible() bool { + return !w.hidden +} + +// Show this widget so it becomes visible +func (w *Base) Show() { + if !w.hidden { + return // Visible already + } + + w.hidden = false + + impl := w.super() + if impl == nil { + return + } + impl.Refresh() +} + +// Hide this widget so it is no longer visible +func (w *Base) Hide() { + if w.hidden { + return // Hidden already + } + + w.hidden = true + + impl := w.super() + if impl == nil { + return + } + canvas.Refresh(impl) +} + +// Refresh causes this widget to be redrawn in it's current state +func (w *Base) Refresh() { + impl := w.super() + if impl == nil { + return + } + + cache.Renderer(impl).Refresh() +} + +// super will return the actual object that this represents. +// If extended then this is the extending widget, otherwise it is nil. +func (w *Base) super() fyne.Widget { + return w.impl +} + +// Repaint instructs the containing canvas to redraw, even if nothing changed. +// This method is a duplicate of what is in `canvas/canvas.go` to avoid a dependency loop or public API. +func Repaint(obj fyne.CanvasObject) { + app := fyne.CurrentApp() + if app == nil || app.Driver() == nil { + return + } + + c := app.Driver().CanvasForObject(obj) + if c != nil { + if paint, ok := c.(interface{ SetDirty() }); ok { + paint.SetDirty() + } + } +} diff --git a/vendor/fyne.io/fyne/v2/internal/widget/base_renderer.go b/vendor/fyne.io/fyne/v2/internal/widget/base_renderer.go new file mode 100644 index 0000000..6fc90e0 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/widget/base_renderer.go @@ -0,0 +1,27 @@ +package widget + +import "fyne.io/fyne/v2" + +// BaseRenderer is a renderer base that provides part of the widget.Renderer interface. +type BaseRenderer struct { + objects []fyne.CanvasObject +} + +// NewBaseRenderer creates a new BaseRenderer. +func NewBaseRenderer(objects []fyne.CanvasObject) BaseRenderer { + return BaseRenderer{objects} +} + +// Destroy does nothing in the base implementation. +func (r *BaseRenderer) Destroy() { +} + +// Objects returns the objects that should be rendered. +func (r *BaseRenderer) Objects() []fyne.CanvasObject { + return r.objects +} + +// SetObjects updates the objects of the renderer. +func (r *BaseRenderer) SetObjects(objects []fyne.CanvasObject) { + r.objects = objects +} diff --git a/vendor/fyne.io/fyne/v2/internal/widget/overlay_container.go b/vendor/fyne.io/fyne/v2/internal/widget/overlay_container.go new file mode 100644 index 0000000..0ea4bb2 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/widget/overlay_container.go @@ -0,0 +1,89 @@ +package widget + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/driver/desktop" +) + +var ( + _ fyne.Widget = (*OverlayContainer)(nil) + _ fyne.Tappable = (*OverlayContainer)(nil) + _ desktop.Hoverable = (*OverlayContainer)(nil) +) + +// OverlayContainer is a transparent widget containing one fyne.CanvasObject and meant to be used as overlay. +type OverlayContainer struct { + Base + Content fyne.CanvasObject + + canvas fyne.Canvas + onDismiss func() + shown bool +} + +// NewOverlayContainer creates an OverlayContainer. +func NewOverlayContainer(c fyne.CanvasObject, canvas fyne.Canvas, onDismiss func()) *OverlayContainer { + o := &OverlayContainer{canvas: canvas, Content: c, onDismiss: onDismiss} + o.ExtendBaseWidget(o) + return o +} + +// CreateRenderer returns a new renderer for the overlay container. +func (o *OverlayContainer) CreateRenderer() fyne.WidgetRenderer { + return &overlayRenderer{BaseRenderer{[]fyne.CanvasObject{o.Content}}, o} +} + +// Hide hides the overlay container. +func (o *OverlayContainer) Hide() { + if o.shown { + o.canvas.Overlays().Remove(o) + o.shown = false + } + o.Base.Hide() +} + +// MouseIn catches mouse-in events not handled by the container’s content. It does nothing. +func (o *OverlayContainer) MouseIn(*desktop.MouseEvent) { +} + +// MouseMoved catches mouse-moved events not handled by the container’s content. It does nothing. +func (o *OverlayContainer) MouseMoved(*desktop.MouseEvent) { +} + +// MouseOut catches mouse-out events not handled by the container’s content. It does nothing. +func (o *OverlayContainer) MouseOut() { +} + +// Show makes the overlay container visible. +func (o *OverlayContainer) Show() { + if !o.shown { + o.canvas.Overlays().Add(o) + o.shown = true + } + o.Base.Show() +} + +// Tapped catches tap events not handled by the container’s content. +// It performs the overlay container’s dismiss action. +func (o *OverlayContainer) Tapped(*fyne.PointEvent) { + if o.onDismiss != nil { + o.onDismiss() + } +} + +type overlayRenderer struct { + BaseRenderer + o *OverlayContainer +} + +var _ fyne.WidgetRenderer = (*overlayRenderer)(nil) + +func (r *overlayRenderer) Layout(fyne.Size) { +} + +func (r *overlayRenderer) MinSize() fyne.Size { + return r.o.canvas.Size() +} + +func (r *overlayRenderer) Refresh() { +} diff --git a/vendor/fyne.io/fyne/v2/internal/widget/scroller.go b/vendor/fyne.io/fyne/v2/internal/widget/scroller.go new file mode 100644 index 0000000..8e1b758 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/widget/scroller.go @@ -0,0 +1,687 @@ +package widget + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/driver/desktop" + "fyne.io/fyne/v2/internal/cache" + "fyne.io/fyne/v2/theme" +) + +// ScrollDirection represents the directions in which a Scroll can scroll its child content. +type ScrollDirection = fyne.ScrollDirection + +// Constants for valid values of ScrollDirection. +const ( + // ScrollBoth supports horizontal and vertical scrolling. + ScrollBoth ScrollDirection = iota + // ScrollHorizontalOnly specifies the scrolling should only happen left to right. + ScrollHorizontalOnly + // ScrollVerticalOnly specifies the scrolling should only happen top to bottom. + ScrollVerticalOnly + // ScrollNone turns off scrolling for this container. + // + // Since: 2.0 + ScrollNone +) + +type scrollBarOrientation int + +// We default to vertical as 0 due to that being the original orientation offered +const ( + scrollBarOrientationVertical scrollBarOrientation = 0 + scrollBarOrientationHorizontal scrollBarOrientation = 1 + scrollContainerMinSize = float32(32) // TODO consider the smallest useful scroll view? + + // what fraction of the page to scroll when tapping on the scroll bar area + pageScrollFraction = float32(0.95) +) + +type scrollBarRenderer struct { + BaseRenderer + scrollBar *scrollBar + background *canvas.Rectangle + minSize fyne.Size +} + +func (r *scrollBarRenderer) Layout(size fyne.Size) { + r.background.Resize(size) +} + +func (r *scrollBarRenderer) MinSize() fyne.Size { + return r.minSize +} + +func (r *scrollBarRenderer) Refresh() { + th := theme.CurrentForWidget(r.scrollBar) + v := fyne.CurrentApp().Settings().ThemeVariant() + + r.background.FillColor = th.Color(theme.ColorNameScrollBar, v) + r.background.CornerRadius = th.Size(theme.SizeNameScrollBarRadius) + r.background.Refresh() +} + +var ( + _ desktop.Hoverable = (*scrollBar)(nil) + _ fyne.Draggable = (*scrollBar)(nil) +) + +type scrollBar struct { + Base + area *scrollBarArea + draggedDistance float32 + dragStart float32 + orientation scrollBarOrientation +} + +func (b *scrollBar) CreateRenderer() fyne.WidgetRenderer { + th := theme.CurrentForWidget(b) + v := fyne.CurrentApp().Settings().ThemeVariant() + + background := canvas.NewRectangle(th.Color(theme.ColorNameScrollBar, v)) + background.CornerRadius = th.Size(theme.SizeNameScrollBarRadius) + r := &scrollBarRenderer{ + scrollBar: b, + background: background, + } + r.SetObjects([]fyne.CanvasObject{background}) + return r +} + +func (b *scrollBar) Cursor() desktop.Cursor { + return desktop.DefaultCursor +} + +func (b *scrollBar) DragEnd() { + b.area.isDragging = false + + if fyne.CurrentDevice().IsMobile() { + b.area.MouseOut() + return + } + b.area.Refresh() +} + +func (b *scrollBar) Dragged(e *fyne.DragEvent) { + if !b.area.isDragging { + b.area.isDragging = true + b.area.MouseIn(nil) + + switch b.orientation { + case scrollBarOrientationHorizontal: + b.dragStart = b.Position().X + case scrollBarOrientationVertical: + b.dragStart = b.Position().Y + } + b.draggedDistance = 0 + } + + switch b.orientation { + case scrollBarOrientationHorizontal: + b.draggedDistance += e.Dragged.DX + case scrollBarOrientationVertical: + b.draggedDistance += e.Dragged.DY + } + b.area.moveBar(b.draggedDistance+b.dragStart, b.Size()) +} + +func (b *scrollBar) MouseIn(e *desktop.MouseEvent) { + b.area.MouseIn(e) +} + +func (b *scrollBar) MouseMoved(*desktop.MouseEvent) { +} + +func (b *scrollBar) MouseOut() { + b.area.MouseOut() +} + +func newScrollBar(area *scrollBarArea) *scrollBar { + b := &scrollBar{area: area, orientation: area.orientation} + b.ExtendBaseWidget(b) + return b +} + +func (a *scrollBarArea) isLarge() bool { + return a.isMouseIn || a.isDragging +} + +type scrollBarAreaRenderer struct { + BaseRenderer + area *scrollBarArea + bar *scrollBar + background *canvas.Rectangle +} + +func (r *scrollBarAreaRenderer) Layout(size fyne.Size) { + r.layoutWithTheme(theme.CurrentForWidget(r.area), size) +} + +func (r *scrollBarAreaRenderer) layoutWithTheme(th fyne.Theme, size fyne.Size) { + var barHeight, barWidth, barX, barY float32 + var bkgHeight, bkgWidth, bkgX, bkgY float32 + switch r.area.orientation { + case scrollBarOrientationHorizontal: + barWidth, barHeight, barX, barY = r.barSizeAndOffset(th, r.area.scroll.Offset.X, r.area.scroll.Content.Size().Width, r.area.scroll.Size().Width) + r.area.barLeadingEdge = barX + r.area.barTrailingEdge = barX + barWidth + bkgWidth, bkgHeight, bkgX, bkgY = size.Width, barHeight, 0, barY + default: + barHeight, barWidth, barY, barX = r.barSizeAndOffset(th, r.area.scroll.Offset.Y, r.area.scroll.Content.Size().Height, r.area.scroll.Size().Height) + r.area.barLeadingEdge = barY + r.area.barTrailingEdge = barY + barHeight + bkgWidth, bkgHeight, bkgX, bkgY = barWidth, size.Height, barX, 0 + } + r.bar.Move(fyne.NewPos(barX, barY)) + r.bar.Resize(fyne.NewSize(barWidth, barHeight)) + r.background.Move(fyne.NewPos(bkgX, bkgY)) + r.background.Resize(fyne.NewSize(bkgWidth, bkgHeight)) +} + +func (r *scrollBarAreaRenderer) MinSize() fyne.Size { + th := theme.CurrentForWidget(r.area) + + barSize := th.Size(theme.SizeNameScrollBar) + min := barSize + if !r.area.isLarge() { + min = th.Size(theme.SizeNameScrollBarSmall) * 2 + } + switch r.area.orientation { + case scrollBarOrientationHorizontal: + return fyne.NewSize(barSize, min) + default: + return fyne.NewSize(min, barSize) + } +} + +func (r *scrollBarAreaRenderer) Refresh() { + th := theme.CurrentForWidget(r.area) + r.bar.Refresh() + r.background.FillColor = th.Color(theme.ColorNameScrollBarBackground, fyne.CurrentApp().Settings().ThemeVariant()) + r.background.Hidden = !r.area.isLarge() + r.layoutWithTheme(th, r.area.Size()) + canvas.Refresh(r.bar) + canvas.Refresh(r.background) +} + +func (r *scrollBarAreaRenderer) barSizeAndOffset(th fyne.Theme, contentOffset, contentLength, scrollLength float32) (length, width, lengthOffset, widthOffset float32) { + scrollBarSize := th.Size(theme.SizeNameScrollBar) + if scrollLength < contentLength { + portion := scrollLength / contentLength + length = float32(int(scrollLength)) * portion + length = fyne.Max(length, scrollBarSize) + } else { + length = scrollLength + } + if contentOffset != 0 { + lengthOffset = (scrollLength - length) * (contentOffset / (contentLength - scrollLength)) + } + if r.area.isLarge() { + width = scrollBarSize + } else { + widthOffset = th.Size(theme.SizeNameScrollBarSmall) + width = widthOffset + } + return length, width, lengthOffset, widthOffset +} + +var ( + _ desktop.Hoverable = (*scrollBarArea)(nil) + _ fyne.Tappable = (*scrollBarArea)(nil) +) + +type scrollBarArea struct { + Base + + isDragging bool + isMouseIn bool + scroll *Scroll + bar *scrollBar + orientation scrollBarOrientation + + // updated from renderer Layout + // coordinates Y in vertical orientation, X in horizontal + barLeadingEdge float32 + barTrailingEdge float32 +} + +func (a *scrollBarArea) CreateRenderer() fyne.WidgetRenderer { + th := theme.CurrentForWidget(a) + v := fyne.CurrentApp().Settings().ThemeVariant() + a.bar = newScrollBar(a) + background := canvas.NewRectangle(th.Color(theme.ColorNameScrollBarBackground, v)) + background.Hidden = !a.isLarge() + return &scrollBarAreaRenderer{BaseRenderer: NewBaseRenderer([]fyne.CanvasObject{background, a.bar}), area: a, bar: a.bar, background: background} +} + +func (a *scrollBarArea) Tapped(e *fyne.PointEvent) { + if isScrollerPageOnTap() { + a.scrollFullPageOnTap(e) + return + } + + // scroll to tapped position + barSize := a.bar.Size() + switch a.orientation { + case scrollBarOrientationHorizontal: + if e.Position.X < a.barLeadingEdge || e.Position.X > a.barTrailingEdge { + a.moveBar(fyne.Max(0, e.Position.X-barSize.Width/2), barSize) + } + case scrollBarOrientationVertical: + if e.Position.Y < a.barLeadingEdge || e.Position.Y > a.barTrailingEdge { + a.moveBar(fyne.Max(0, e.Position.Y-barSize.Height/2), a.bar.Size()) + } + } +} + +func (a *scrollBarArea) scrollFullPageOnTap(e *fyne.PointEvent) { + // when tapping above/below or left/right of the bar, scroll the content + // nearly a full page (pageScrollFraction) up/down or left/right, respectively + newOffset := a.scroll.Offset + switch a.orientation { + case scrollBarOrientationHorizontal: + if e.Position.X < a.barLeadingEdge { + newOffset.X = fyne.Max(0, newOffset.X-a.scroll.Size().Width*pageScrollFraction) + } else if e.Position.X > a.barTrailingEdge { + viewWid := a.scroll.Size().Width + newOffset.X = fyne.Min(a.scroll.Content.Size().Width-viewWid, newOffset.X+viewWid*pageScrollFraction) + } + default: + if e.Position.Y < a.barLeadingEdge { + newOffset.Y = fyne.Max(0, newOffset.Y-a.scroll.Size().Height*pageScrollFraction) + } else if e.Position.Y > a.barTrailingEdge { + viewHt := a.scroll.Size().Height + newOffset.Y = fyne.Min(a.scroll.Content.Size().Height-viewHt, newOffset.Y+viewHt*pageScrollFraction) + } + } + if newOffset == a.scroll.Offset { + return + } + + a.scroll.Offset = newOffset + if f := a.scroll.OnScrolled; f != nil { + f(a.scroll.Offset) + } + a.scroll.refreshWithoutOffsetUpdate() +} + +func (a *scrollBarArea) MouseIn(*desktop.MouseEvent) { + a.isMouseIn = true + a.scroll.refreshBars() +} + +func (a *scrollBarArea) MouseMoved(*desktop.MouseEvent) { +} + +func (a *scrollBarArea) MouseOut() { + a.isMouseIn = false + if a.isDragging { + return + } + + a.scroll.refreshBars() +} + +func (a *scrollBarArea) moveBar(offset float32, barSize fyne.Size) { + oldX := a.scroll.Offset.X + oldY := a.scroll.Offset.Y + switch a.orientation { + case scrollBarOrientationHorizontal: + a.scroll.Offset.X = a.computeScrollOffset(barSize.Width, offset, a.scroll.Size().Width, a.scroll.Content.Size().Width) + default: + a.scroll.Offset.Y = a.computeScrollOffset(barSize.Height, offset, a.scroll.Size().Height, a.scroll.Content.Size().Height) + } + if f := a.scroll.OnScrolled; f != nil && (a.scroll.Offset.X != oldX || a.scroll.Offset.Y != oldY) { + f(a.scroll.Offset) + } + a.scroll.refreshWithoutOffsetUpdate() +} + +func (a *scrollBarArea) computeScrollOffset(length, offset, scrollLength, contentLength float32) float32 { + maxOffset := scrollLength - length + if offset < 0 { + offset = 0 + } else if offset > maxOffset { + offset = maxOffset + } + ratio := offset / maxOffset + scrollOffset := ratio * (contentLength - scrollLength) + return scrollOffset +} + +func newScrollBarArea(scroll *Scroll, orientation scrollBarOrientation) *scrollBarArea { + a := &scrollBarArea{scroll: scroll, orientation: orientation} + a.ExtendBaseWidget(a) + return a +} + +type scrollContainerRenderer struct { + BaseRenderer + scroll *Scroll + vertArea *scrollBarArea + horizArea *scrollBarArea + leftShadow, rightShadow *Shadow + topShadow, bottomShadow *Shadow + oldMinSize fyne.Size +} + +func (r *scrollContainerRenderer) layoutBars(size fyne.Size) { + scrollerSize := r.scroll.Size() + if r.scroll.Direction == ScrollVerticalOnly || r.scroll.Direction == ScrollBoth { + r.vertArea.Resize(fyne.NewSize(r.vertArea.MinSize().Width, size.Height)) + r.vertArea.Move(fyne.NewPos(scrollerSize.Width-r.vertArea.Size().Width, 0)) + r.topShadow.Resize(fyne.NewSize(size.Width, 0)) + r.bottomShadow.Resize(fyne.NewSize(size.Width, 0)) + r.bottomShadow.Move(fyne.NewPos(0, scrollerSize.Height)) + } + + if r.scroll.Direction == ScrollHorizontalOnly || r.scroll.Direction == ScrollBoth { + r.horizArea.Resize(fyne.NewSize(size.Width, r.horizArea.MinSize().Height)) + r.horizArea.Move(fyne.NewPos(0, scrollerSize.Height-r.horizArea.Size().Height)) + r.leftShadow.Resize(fyne.NewSize(0, size.Height)) + r.rightShadow.Resize(fyne.NewSize(0, size.Height)) + r.rightShadow.Move(fyne.NewPos(scrollerSize.Width, 0)) + } + + r.updatePosition() +} + +func (r *scrollContainerRenderer) Layout(size fyne.Size) { + c := r.scroll.Content + c.Resize(c.MinSize().Max(size)) + + r.layoutBars(size) +} + +func (r *scrollContainerRenderer) MinSize() fyne.Size { + return r.scroll.MinSize() +} + +func (r *scrollContainerRenderer) Refresh() { + r.horizArea.Refresh() + r.vertArea.Refresh() + r.leftShadow.Refresh() + r.topShadow.Refresh() + r.rightShadow.Refresh() + r.bottomShadow.Refresh() + + if len(r.BaseRenderer.Objects()) == 0 || r.BaseRenderer.Objects()[0] != r.scroll.Content { + // push updated content object to baseRenderer + r.BaseRenderer.Objects()[0] = r.scroll.Content + } + size := r.scroll.Size() + newMin := r.scroll.Content.MinSize() + if r.oldMinSize == newMin && r.oldMinSize == r.scroll.Content.Size() && + (size.Width <= r.oldMinSize.Width && size.Height <= r.oldMinSize.Height) { + r.layoutBars(size) + return + } + + r.oldMinSize = newMin + r.Layout(size) +} + +func (r *scrollContainerRenderer) handleAreaVisibility(contentSize, scrollSize float32, area *scrollBarArea) { + if contentSize <= scrollSize { + area.Hide() + } else if r.scroll.Visible() { + area.Show() + } +} + +func (r *scrollContainerRenderer) handleShadowVisibility(offset, contentSize, scrollSize float32, shadowStart fyne.CanvasObject, shadowEnd fyne.CanvasObject) { + if !r.scroll.Visible() { + return + } + if offset > 0 { + shadowStart.Show() + } else { + shadowStart.Hide() + } + if offset < contentSize-scrollSize { + shadowEnd.Show() + } else { + shadowEnd.Hide() + } +} + +func (r *scrollContainerRenderer) updatePosition() { + if r.scroll.Content == nil { + return + } + scrollSize := r.scroll.Size() + contentSize := r.scroll.Content.Size() + + r.scroll.Content.Move(fyne.NewPos(-r.scroll.Offset.X, -r.scroll.Offset.Y)) + + if r.scroll.Direction == ScrollVerticalOnly || r.scroll.Direction == ScrollBoth { + r.handleAreaVisibility(contentSize.Height, scrollSize.Height, r.vertArea) + r.handleShadowVisibility(r.scroll.Offset.Y, contentSize.Height, scrollSize.Height, r.topShadow, r.bottomShadow) + cache.Renderer(r.vertArea).Layout(scrollSize) + } else { + r.vertArea.Hide() + r.topShadow.Hide() + r.bottomShadow.Hide() + } + if r.scroll.Direction == ScrollHorizontalOnly || r.scroll.Direction == ScrollBoth { + r.handleAreaVisibility(contentSize.Width, scrollSize.Width, r.horizArea) + r.handleShadowVisibility(r.scroll.Offset.X, contentSize.Width, scrollSize.Width, r.leftShadow, r.rightShadow) + cache.Renderer(r.horizArea).Layout(scrollSize) + } else { + r.horizArea.Hide() + r.leftShadow.Hide() + r.rightShadow.Hide() + } + + if r.scroll.Direction != ScrollHorizontalOnly { + canvas.Refresh(r.vertArea) // this is required to force the canvas to update, we have no "Redraw()" + } else { + canvas.Refresh(r.horizArea) // this is required like above but if we are horizontal + } +} + +// Scroll defines a container that is smaller than the Content. +// The Offset is used to determine the position of the child widgets within the container. +type Scroll struct { + Base + minSize fyne.Size + Direction ScrollDirection + Content fyne.CanvasObject + Offset fyne.Position + // OnScrolled can be set to be notified when the Scroll has changed position. + // You should not update the Scroll.Offset from this method. + // + // Since: 2.0 + OnScrolled func(fyne.Position) `json:"-"` +} + +// CreateRenderer is a private method to Fyne which links this widget to its renderer +func (s *Scroll) CreateRenderer() fyne.WidgetRenderer { + scr := &scrollContainerRenderer{ + BaseRenderer: NewBaseRenderer([]fyne.CanvasObject{s.Content}), + scroll: s, + } + scr.vertArea = newScrollBarArea(s, scrollBarOrientationVertical) + scr.topShadow = NewShadow(ShadowBottom, SubmergedContentLevel) + scr.bottomShadow = NewShadow(ShadowTop, SubmergedContentLevel) + scr.horizArea = newScrollBarArea(s, scrollBarOrientationHorizontal) + scr.leftShadow = NewShadow(ShadowRight, SubmergedContentLevel) + scr.rightShadow = NewShadow(ShadowLeft, SubmergedContentLevel) + scr.SetObjects(append(scr.Objects(), scr.topShadow, scr.bottomShadow, scr.leftShadow, scr.rightShadow, + scr.vertArea, scr.horizArea)) + scr.updatePosition() + + return scr +} + +// ScrollToBottom will scroll content to container bottom - to show latest info which end user just added +func (s *Scroll) ScrollToBottom() { + s.scrollBy(0, -1*(s.Content.MinSize().Height-s.Size().Height-s.Offset.Y)) + s.refreshBars() +} + +// ScrollToTop will scroll content to container top +func (s *Scroll) ScrollToTop() { + s.ScrollToOffset(fyne.Position{}) + s.refreshBars() +} + +// DragEnd will stop scrolling on mobile has stopped +func (s *Scroll) DragEnd() { +} + +// Dragged will scroll on any drag - bar or otherwise - for mobile +func (s *Scroll) Dragged(e *fyne.DragEvent) { + if !fyne.CurrentDevice().IsMobile() { + return + } + + if s.updateOffset(e.Dragged.DX, e.Dragged.DY) { + s.refreshWithoutOffsetUpdate() + } +} + +// MinSize returns the smallest size this widget can shrink to +func (s *Scroll) MinSize() fyne.Size { + min := fyne.NewSize(scrollContainerMinSize, scrollContainerMinSize).Max(s.minSize) + switch s.Direction { + case ScrollHorizontalOnly: + min.Height = fyne.Max(min.Height, s.Content.MinSize().Height) + case ScrollVerticalOnly: + min.Width = fyne.Max(min.Width, s.Content.MinSize().Width) + case ScrollNone: + return s.Content.MinSize() + } + return min +} + +// SetMinSize specifies a minimum size for this scroll container. +// If the specified size is larger than the content size then scrolling will not be enabled +// This can be helpful to appear larger than default if the layout is collapsing this widget. +func (s *Scroll) SetMinSize(size fyne.Size) { + s.minSize = size +} + +// Refresh causes this widget to be redrawn in it's current state +func (s *Scroll) Refresh() { + s.refreshBars() + + if s.Content != nil { + s.Content.Refresh() + } +} + +// Resize is called when this scroller should change size. We refresh to ensure the scroll bars are updated. +func (s *Scroll) Resize(sz fyne.Size) { + if sz == s.Size() { + return + } + + s.Base.Resize(sz) + s.refreshBars() +} + +// ScrollToOffset will update the location of the content of this scroll container. +// +// Since: 2.6 +func (s *Scroll) ScrollToOffset(p fyne.Position) { + if s.Offset == p { + return + } + + s.Offset = p + s.refreshBars() +} + +func (s *Scroll) refreshWithoutOffsetUpdate() { + s.Base.Refresh() +} + +// Scrolled is called when an input device triggers a scroll event +func (s *Scroll) Scrolled(ev *fyne.ScrollEvent) { + if s.Direction != ScrollNone { + s.scrollBy(ev.Scrolled.DX, ev.Scrolled.DY) + } +} + +func (s *Scroll) refreshBars() { + s.updateOffset(0, 0) + s.refreshWithoutOffsetUpdate() +} + +func (s *Scroll) scrollBy(dx, dy float32) { + min := s.Content.MinSize() + size := s.Size() + if size.Width < min.Width && size.Height >= min.Height && dx == 0 { + dx, dy = dy, dx + } + if s.updateOffset(dx, dy) { + s.refreshWithoutOffsetUpdate() + } +} + +func (s *Scroll) updateOffset(deltaX, deltaY float32) bool { + size := s.Size() + contentSize := s.Content.Size() + if contentSize.Width <= size.Width && contentSize.Height <= size.Height { + if s.Offset.X != 0 || s.Offset.Y != 0 { + s.Offset.X = 0 + s.Offset.Y = 0 + return true + } + return false + } + oldX := s.Offset.X + oldY := s.Offset.Y + min := s.Content.MinSize() + s.Offset.X = computeOffset(s.Offset.X, -deltaX, size.Width, min.Width) + s.Offset.Y = computeOffset(s.Offset.Y, -deltaY, size.Height, min.Height) + + moved := s.Offset.X != oldX || s.Offset.Y != oldY + if f := s.OnScrolled; f != nil && moved { + f(s.Offset) + } + return moved +} + +func computeOffset(start, delta, outerWidth, innerWidth float32) float32 { + offset := start + delta + if offset+outerWidth >= innerWidth { + offset = innerWidth - outerWidth + } + + return fyne.Max(offset, 0) +} + +// NewScroll creates a scrollable parent wrapping the specified content. +// Note that this may cause the MinSize to be smaller than that of the passed object. +func NewScroll(content fyne.CanvasObject) *Scroll { + s := newScrollContainerWithDirection(ScrollBoth, content) + s.ExtendBaseWidget(s) + return s +} + +// NewHScroll create a scrollable parent wrapping the specified content. +// Note that this may cause the MinSize.Width to be smaller than that of the passed object. +func NewHScroll(content fyne.CanvasObject) *Scroll { + s := newScrollContainerWithDirection(ScrollHorizontalOnly, content) + s.ExtendBaseWidget(s) + return s +} + +// NewVScroll create a scrollable parent wrapping the specified content. +// Note that this may cause the MinSize.Height to be smaller than that of the passed object. +func NewVScroll(content fyne.CanvasObject) *Scroll { + s := newScrollContainerWithDirection(ScrollVerticalOnly, content) + s.ExtendBaseWidget(s) + return s +} + +func newScrollContainerWithDirection(direction ScrollDirection, content fyne.CanvasObject) *Scroll { + s := &Scroll{ + Direction: direction, + Content: content, + } + s.ExtendBaseWidget(s) + return s +} diff --git a/vendor/fyne.io/fyne/v2/internal/widget/scroller_behavior_darwin.go b/vendor/fyne.io/fyne/v2/internal/widget/scroller_behavior_darwin.go new file mode 100644 index 0000000..cee48bd --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/widget/scroller_behavior_darwin.go @@ -0,0 +1,12 @@ +//go:build darwin + +package widget + +/* +int getScrollerPagingBehavior(); +*/ +import "C" + +func isScrollerPageOnTap() bool { + return C.getScrollerPagingBehavior() == 0 +} diff --git a/vendor/fyne.io/fyne/v2/internal/widget/scroller_behavior_darwin.m b/vendor/fyne.io/fyne/v2/internal/widget/scroller_behavior_darwin.m new file mode 100644 index 0000000..63eae18 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/widget/scroller_behavior_darwin.m @@ -0,0 +1,7 @@ +//go:build darwin + +#import + +int getScrollerPagingBehavior() { + return [[NSUserDefaults standardUserDefaults] boolForKey:@"AppleScrollerPagingBehavior"]; +} diff --git a/vendor/fyne.io/fyne/v2/internal/widget/scroller_behavior_other.go b/vendor/fyne.io/fyne/v2/internal/widget/scroller_behavior_other.go new file mode 100644 index 0000000..dbde695 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/widget/scroller_behavior_other.go @@ -0,0 +1,7 @@ +//go:build !darwin + +package widget + +func isScrollerPageOnTap() bool { + return false +} diff --git a/vendor/fyne.io/fyne/v2/internal/widget/shadow.go b/vendor/fyne.io/fyne/v2/internal/widget/shadow.go new file mode 100644 index 0000000..484aacf --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/widget/shadow.go @@ -0,0 +1,201 @@ +package widget + +import ( + "image/color" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/theme" +) + +var _ fyne.Widget = (*Shadow)(nil) + +// Shadow is a widget that renders a shadow. +type Shadow struct { + Base + level ElevationLevel + typ ShadowType +} + +// ElevationLevel is the level of elevation of the shadow casting object. +type ElevationLevel int + +// ElevationLevel constants inspired by: +// https://storage.googleapis.com/spec-host/mio-staging%2Fmio-design%2F1584058305895%2Fassets%2F0B6xUSjjSulxceF9udnA4Sk5tdU0%2Fbaselineelevation-chart.png +const ( + BaseLevel ElevationLevel = 0 + CardLevel ElevationLevel = 1 + ButtonLevel ElevationLevel = 2 + MenuLevel ElevationLevel = 4 + PopUpLevel ElevationLevel = 8 + SubmergedContentLevel ElevationLevel = 8 + DialogLevel ElevationLevel = 24 +) + +// ShadowType specifies the type of the shadow. +type ShadowType int + +// ShadowType constants +const ( + ShadowAround ShadowType = iota + ShadowLeft + ShadowRight + ShadowBottom + ShadowTop +) + +// NewShadow create a new Shadow. +func NewShadow(typ ShadowType, level ElevationLevel) *Shadow { + s := &Shadow{typ: typ, level: level} + s.ExtendBaseWidget(s) + return s +} + +// CreateRenderer returns a new renderer for the shadow. +func (s *Shadow) CreateRenderer() fyne.WidgetRenderer { + r := &shadowRenderer{s: s} + r.createShadows() + return r +} + +type shadowRenderer struct { + BaseRenderer + b, l, r, t *canvas.LinearGradient + bl, br, tl, tr *canvas.RadialGradient + minSize fyne.Size + s *Shadow +} + +func (r *shadowRenderer) Layout(size fyne.Size) { + depth := float32(r.s.level) + sideOff, topOff := float32(0.0), float32(0.0) + if r.s.typ == ShadowAround { + sideOff = depth * 0.2 + topOff = sideOff * 2 + } + + if r.tl != nil { + r.tl.Resize(fyne.NewSize(depth, depth)) + r.tl.Move(fyne.NewPos(-depth+sideOff, -depth+topOff)) + } + if r.t != nil { + r.t.Resize(fyne.NewSize(size.Width-sideOff*2, depth)) + r.t.Move(fyne.NewPos(sideOff, -depth+topOff)) + } + if r.tr != nil { + r.tr.Resize(fyne.NewSize(depth, depth)) + r.tr.Move(fyne.NewPos(size.Width-sideOff, -depth+topOff)) + } + if r.r != nil { + r.r.Resize(fyne.NewSize(depth, size.Height-topOff)) + r.r.Move(fyne.NewPos(size.Width-sideOff, topOff)) + } + if r.br != nil { + r.br.Resize(fyne.NewSize(depth, depth)) + r.br.Move(fyne.NewPos(size.Width-sideOff, size.Height)) + } + if r.b != nil { + r.b.Resize(fyne.NewSize(size.Width-sideOff*2, depth)) + r.b.Move(fyne.NewPos(sideOff, size.Height)) + } + if r.bl != nil { + r.bl.Resize(fyne.NewSize(depth, depth)) + r.bl.Move(fyne.NewPos(-depth+sideOff, size.Height)) + } + if r.l != nil { + r.l.Resize(fyne.NewSize(depth, size.Height-topOff)) + r.l.Move(fyne.NewPos(-depth+sideOff, topOff)) + } +} + +func (r *shadowRenderer) MinSize() fyne.Size { + return r.minSize +} + +func (r *shadowRenderer) Refresh() { + r.refreshShadows() + r.Layout(r.s.Size()) + canvas.Refresh(r.s) +} + +func (r *shadowRenderer) createShadows() { + th := theme.CurrentForWidget(r.s) + v := fyne.CurrentApp().Settings().ThemeVariant() + fg := th.Color(theme.ColorNameShadow, v) + + switch r.s.typ { + case ShadowLeft: + r.l = canvas.NewHorizontalGradient(color.Transparent, fg) + r.SetObjects([]fyne.CanvasObject{r.l}) + case ShadowRight: + r.r = canvas.NewHorizontalGradient(fg, color.Transparent) + r.SetObjects([]fyne.CanvasObject{r.r}) + case ShadowBottom: + r.b = canvas.NewVerticalGradient(fg, color.Transparent) + r.SetObjects([]fyne.CanvasObject{r.b}) + case ShadowTop: + r.t = canvas.NewVerticalGradient(color.Transparent, fg) + r.SetObjects([]fyne.CanvasObject{r.t}) + case ShadowAround: + r.tl = canvas.NewRadialGradient(fg, color.Transparent) + r.tl.CenterOffsetX = 0.5 + r.tl.CenterOffsetY = 0.5 + r.t = canvas.NewVerticalGradient(color.Transparent, fg) + r.tr = canvas.NewRadialGradient(fg, color.Transparent) + r.tr.CenterOffsetX = -0.5 + r.tr.CenterOffsetY = 0.5 + r.r = canvas.NewHorizontalGradient(fg, color.Transparent) + r.br = canvas.NewRadialGradient(fg, color.Transparent) + r.br.CenterOffsetX = -0.5 + r.br.CenterOffsetY = -0.5 + r.b = canvas.NewVerticalGradient(fg, color.Transparent) + r.bl = canvas.NewRadialGradient(fg, color.Transparent) + r.bl.CenterOffsetX = 0.5 + r.bl.CenterOffsetY = -0.5 + r.l = canvas.NewHorizontalGradient(color.Transparent, fg) + r.SetObjects([]fyne.CanvasObject{r.tl, r.t, r.tr, r.r, r.br, r.b, r.bl, r.l}) + } +} + +func (r *shadowRenderer) refreshShadows() { + th := theme.CurrentForWidget(r.s) + v := fyne.CurrentApp().Settings().ThemeVariant() + fg := th.Color(theme.ColorNameShadow, v) + + updateShadowEnd(r.l, fg) + updateShadowStart(r.r, fg) + updateShadowStart(r.b, fg) + updateShadowEnd(r.t, fg) + + updateShadowRadial(r.tl, fg) + updateShadowRadial(r.tr, fg) + updateShadowRadial(r.bl, fg) + updateShadowRadial(r.br, fg) +} + +func updateShadowEnd(g *canvas.LinearGradient, fg color.Color) { + if g == nil { + return + } + + g.EndColor = fg + g.Refresh() +} + +func updateShadowRadial(g *canvas.RadialGradient, fg color.Color) { + if g == nil { + return + } + + g.StartColor = fg + g.Refresh() +} + +func updateShadowStart(g *canvas.LinearGradient, fg color.Color) { + if g == nil { + return + } + + g.StartColor = fg + g.Refresh() +} diff --git a/vendor/fyne.io/fyne/v2/internal/widget/shadowing_renderer.go b/vendor/fyne.io/fyne/v2/internal/widget/shadowing_renderer.go new file mode 100644 index 0000000..6266a03 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/widget/shadowing_renderer.go @@ -0,0 +1,49 @@ +package widget + +import ( + "fyne.io/fyne/v2" +) + +// ShadowingRenderer is a renderer that adds a shadow around the rendered content. +// When using the ShadowingRenderer the embedding renderer should call +// LayoutShadow(contentSize, contentPos) to lay out the shadow. +type ShadowingRenderer struct { + BaseRenderer + shadow fyne.CanvasObject +} + +// NewShadowingRenderer creates a ShadowingRenderer. +func NewShadowingRenderer(objects []fyne.CanvasObject, level ElevationLevel) *ShadowingRenderer { + var s fyne.CanvasObject + if level > 0 { + s = NewShadow(ShadowAround, level) + } + r := &ShadowingRenderer{shadow: s} + r.SetObjects(objects) + return r +} + +// LayoutShadow adjusts the size and position of the shadow if necessary. +func (r *ShadowingRenderer) LayoutShadow(size fyne.Size, pos fyne.Position) { + if r.shadow == nil { + return + } + r.shadow.Resize(size) + r.shadow.Move(pos) +} + +// SetObjects updates the renderer's objects including the shadow if necessary. +func (r *ShadowingRenderer) SetObjects(objects []fyne.CanvasObject) { + if r.shadow != nil && len(objects) > 0 && r.shadow != objects[0] { + objects = append([]fyne.CanvasObject{r.shadow}, objects...) + } + r.BaseRenderer.SetObjects(objects) +} + +// RefreshShadow asks the shadow graphical element to update to current theme +func (r *ShadowingRenderer) RefreshShadow() { + if r.shadow == nil { + return + } + r.shadow.Refresh() +} diff --git a/vendor/fyne.io/fyne/v2/internal/widget/simple_renderer.go b/vendor/fyne.io/fyne/v2/internal/widget/simple_renderer.go new file mode 100644 index 0000000..33d4048 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/internal/widget/simple_renderer.go @@ -0,0 +1,55 @@ +package widget + +import "fyne.io/fyne/v2" + +var _ fyne.WidgetRenderer = (*SimpleRenderer)(nil) + +// SimpleRenderer is a basic renderer that satisfies widget.Renderer interface by wrapping +// a single fyne.CanvasObject. +// +// Since: 2.1 +type SimpleRenderer struct { + objects []fyne.CanvasObject +} + +// NewSimpleRenderer creates a new SimpleRenderer to render a widget using a +// single CanvasObject. +// +// Since: 2.1 +func NewSimpleRenderer(object fyne.CanvasObject) *SimpleRenderer { + return &SimpleRenderer{[]fyne.CanvasObject{object}} +} + +// Destroy does nothing in this implementation. +// +// Since: 2.1 +func (r *SimpleRenderer) Destroy() { +} + +// Layout updates the contained object to be the requested size. +// +// Since: 2.1 +func (r *SimpleRenderer) Layout(s fyne.Size) { + r.objects[0].Resize(s) +} + +// MinSize returns the smallest size that this render can use, returned from the underlying object. +// +// Since: 2.1 +func (r *SimpleRenderer) MinSize() fyne.Size { + return r.objects[0].MinSize() +} + +// Objects returns the objects that should be rendered. +// +// Since: 2.1 +func (r *SimpleRenderer) Objects() []fyne.CanvasObject { + return r.objects +} + +// Refresh requests the underlying object to redraw. +// +// Since: 2.1 +func (r *SimpleRenderer) Refresh() { + r.objects[0].Refresh() +} diff --git a/vendor/fyne.io/fyne/v2/key.go b/vendor/fyne.io/fyne/v2/key.go new file mode 100644 index 0000000..a950eb6 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/key.go @@ -0,0 +1,200 @@ +package fyne + +// KeyName represents the name of a key that has been pressed. +type KeyName string + +const ( + // KeyEscape is the "esc" key + KeyEscape KeyName = "Escape" + // KeyReturn is the carriage return (main keyboard) + KeyReturn KeyName = "Return" + // KeyTab is the tab advance key + KeyTab KeyName = "Tab" + // KeyBackspace is the delete-before-cursor key + KeyBackspace KeyName = "BackSpace" + // KeyInsert is the insert mode key + KeyInsert KeyName = "Insert" + // KeyDelete is the delete-after-cursor key + KeyDelete KeyName = "Delete" + // KeyRight is the right arrow key + KeyRight KeyName = "Right" + // KeyLeft is the left arrow key + KeyLeft KeyName = "Left" + // KeyDown is the down arrow key + KeyDown KeyName = "Down" + // KeyUp is the up arrow key + KeyUp KeyName = "Up" + // KeyPageUp is the page up num-pad key + KeyPageUp KeyName = "Prior" + // KeyPageDown is the page down num-pad key + KeyPageDown KeyName = "Next" + // KeyHome is the line-home key + KeyHome KeyName = "Home" + // KeyEnd is the line-end key + KeyEnd KeyName = "End" + + // KeyF1 is the first function key + KeyF1 KeyName = "F1" + // KeyF2 is the second function key + KeyF2 KeyName = "F2" + // KeyF3 is the third function key + KeyF3 KeyName = "F3" + // KeyF4 is the fourth function key + KeyF4 KeyName = "F4" + // KeyF5 is the fifth function key + KeyF5 KeyName = "F5" + // KeyF6 is the sixth function key + KeyF6 KeyName = "F6" + // KeyF7 is the seventh function key + KeyF7 KeyName = "F7" + // KeyF8 is the eighth function key + KeyF8 KeyName = "F8" + // KeyF9 is the ninth function key + KeyF9 KeyName = "F9" + // KeyF10 is the tenth function key + KeyF10 KeyName = "F10" + // KeyF11 is the eleventh function key + KeyF11 KeyName = "F11" + // KeyF12 is the twelfth function key + KeyF12 KeyName = "F12" + /* + F13 + ... + F25 + */ + + // KeyEnter is the enter/ return key (keypad) + KeyEnter KeyName = "KP_Enter" + + // Key0 represents the key 0 + Key0 KeyName = "0" + // Key1 represents the key 1 + Key1 KeyName = "1" + // Key2 represents the key 2 + Key2 KeyName = "2" + // Key3 represents the key 3 + Key3 KeyName = "3" + // Key4 represents the key 4 + Key4 KeyName = "4" + // Key5 represents the key 5 + Key5 KeyName = "5" + // Key6 represents the key 6 + Key6 KeyName = "6" + // Key7 represents the key 7 + Key7 KeyName = "7" + // Key8 represents the key 8 + Key8 KeyName = "8" + // Key9 represents the key 9 + Key9 KeyName = "9" + // KeyA represents the key A + KeyA KeyName = "A" + // KeyB represents the key B + KeyB KeyName = "B" + // KeyC represents the key C + KeyC KeyName = "C" + // KeyD represents the key D + KeyD KeyName = "D" + // KeyE represents the key E + KeyE KeyName = "E" + // KeyF represents the key F + KeyF KeyName = "F" + // KeyG represents the key G + KeyG KeyName = "G" + // KeyH represents the key H + KeyH KeyName = "H" + // KeyI represents the key I + KeyI KeyName = "I" + // KeyJ represents the key J + KeyJ KeyName = "J" + // KeyK represents the key K + KeyK KeyName = "K" + // KeyL represents the key L + KeyL KeyName = "L" + // KeyM represents the key M + KeyM KeyName = "M" + // KeyN represents the key N + KeyN KeyName = "N" + // KeyO represents the key O + KeyO KeyName = "O" + // KeyP represents the key P + KeyP KeyName = "P" + // KeyQ represents the key Q + KeyQ KeyName = "Q" + // KeyR represents the key R + KeyR KeyName = "R" + // KeyS represents the key S + KeyS KeyName = "S" + // KeyT represents the key T + KeyT KeyName = "T" + // KeyU represents the key U + KeyU KeyName = "U" + // KeyV represents the key V + KeyV KeyName = "V" + // KeyW represents the key W + KeyW KeyName = "W" + // KeyX represents the key X + KeyX KeyName = "X" + // KeyY represents the key Y + KeyY KeyName = "Y" + // KeyZ represents the key Z + KeyZ KeyName = "Z" + + // KeySpace is the space key + KeySpace KeyName = "Space" + // KeyApostrophe is the key "'" + KeyApostrophe KeyName = "'" + // KeyComma is the key "," + KeyComma KeyName = "," + // KeyMinus is the key "-" + KeyMinus KeyName = "-" + // KeyPeriod is the key "." (full stop) + KeyPeriod KeyName = "." + // KeySlash is the key "/" + KeySlash KeyName = "/" + // KeyBackslash is the key "\" + KeyBackslash KeyName = "\\" + // KeyLeftBracket is the key "[" + KeyLeftBracket KeyName = "[" + // KeyRightBracket is the key "]" + KeyRightBracket KeyName = "]" + // KeySemicolon is the key ";" + KeySemicolon KeyName = ";" + // KeyEqual is the key "=" + KeyEqual KeyName = "=" + // KeyAsterisk is the keypad key "*" + KeyAsterisk KeyName = "*" + // KeyPlus is the keypad key "+" + KeyPlus KeyName = "+" + // KeyBackTick is the key "`" on a US keyboard + KeyBackTick KeyName = "`" + + // KeyUnknown is used for key events where the underlying hardware generated an + // event that Fyne could not decode. + // + // Since: 2.1 + KeyUnknown KeyName = "" +) + +// KeyModifier represents any modifier key (shift etc.) that is being pressed together with a key. +// +// Since: 2.2 +type KeyModifier int + +const ( + // KeyModifierShift represents a shift key being held + // + // Since: 2.2 + KeyModifierShift KeyModifier = 1 << iota + // KeyModifierControl represents the ctrl key being held + // + // Since: 2.2 + KeyModifierControl + // KeyModifierAlt represents either alt keys being held + // + // Since: 2.2 + KeyModifierAlt + // KeyModifierSuper represents either super keys being held + // + // Since: 2.2 + KeyModifierSuper +) diff --git a/vendor/fyne.io/fyne/v2/key_darwin.go b/vendor/fyne.io/fyne/v2/key_darwin.go new file mode 100644 index 0000000..e3e4163 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/key_darwin.go @@ -0,0 +1,6 @@ +package fyne + +// KeyModifierShortcutDefault is the default key modifier for shortcuts (Control or Command). +// +// Since: 2.2 +const KeyModifierShortcutDefault = KeyModifierSuper diff --git a/vendor/fyne.io/fyne/v2/key_other.go b/vendor/fyne.io/fyne/v2/key_other.go new file mode 100644 index 0000000..f5fdd0d --- /dev/null +++ b/vendor/fyne.io/fyne/v2/key_other.go @@ -0,0 +1,8 @@ +//go:build !darwin + +package fyne + +// KeyModifierShortcutDefault is the default key modifier for shortcuts (Control or Command). +// +// Since: 2.2 +const KeyModifierShortcutDefault = KeyModifierControl diff --git a/vendor/fyne.io/fyne/v2/lang/lang.go b/vendor/fyne.io/fyne/v2/lang/lang.go new file mode 100644 index 0000000..69dd8ae --- /dev/null +++ b/vendor/fyne.io/fyne/v2/lang/lang.go @@ -0,0 +1,209 @@ +// Package lang introduces a translation and localisation API for Fyne applications +// +// Since 2.5 +package lang + +import ( + "embed" + "encoding/json" + "log" + "strings" + "sync" + "text/template" + + "github.com/jeandeaual/go-locale" + "github.com/nicksnyder/go-i18n/v2/i18n" + + "fyne.io/fyne/v2" + + "golang.org/x/text/language" +) + +var ( + // L is a shortcut to localize a string, similar to the gettext "_" function. + // More info available on the `Localize` function. + L = Localize + + // N is a shortcut to localize a string with plural forms, similar to the ngettext function. + // More info available on the `LocalizePlural` function. + N = LocalizePlural + + // X is a shortcut to get the localization of a string with specified key, similar to pgettext. + // More info available on the `LocalizeKey` function. + X = LocalizeKey + + // XN is a shortcut to get the localization plural form of a string with specified key, similar to npgettext. + // More info available on the `LocalizePluralKey` function. + XN = LocalizePluralKey + + bundle *i18n.Bundle + localizer *i18n.Localizer + setupOnce sync.Once + + //go:embed translations + translations embed.FS + translated []language.Tag +) + +// Localize asks the translation engine to translate a string, this behaves like the gettext "_" function. +// The string can be templated and the template data can be passed as a struct with exported fields, +// or as a map of string keys to any suitable value. +func Localize(in string, data ...any) string { + return LocalizeKey(in, in, data...) +} + +// LocalizeKey asks the translation engine for the translation with specific ID. +// If it cannot be found then the fallback will be used. +// The string can be templated and the template data can be passed as a struct with exported fields, +// or as a map of string keys to any suitable value. +func LocalizeKey(key, fallback string, data ...any) string { + var d0 any + if len(data) > 0 { + d0 = data[0] + } + + ret, err := localizer.Localize(&i18n.LocalizeConfig{ + DefaultMessage: &i18n.Message{ + ID: key, + Other: fallback, + }, + TemplateData: d0, + }) + if err != nil { + fyne.LogError("Translation failure", err) + return fallbackWithData(key, fallback, d0) + } + return ret +} + +// LocalizePlural asks the translation engine to translate a string from one of a number of plural forms. +// This behaves like the ngettext function, with the `count` parameter determining the plurality looked up. +// The string can be templated and the template data can be passed as a struct with exported fields, +// or as a map of string keys to any suitable value. +func LocalizePlural(in string, count int, data ...any) string { + return LocalizePluralKey(in, in, count, data...) +} + +// LocalizePluralKey asks the translation engine for the translation with specific ID in plural form. +// This behaves like the npgettext function, with the `count` parameter determining the plurality looked up. +// If it cannot be found then the fallback will be used. +// The string can be templated and the template data can be passed as a struct with exported fields, +// or as a map of string keys to any suitable value. +func LocalizePluralKey(key, fallback string, count int, data ...any) string { + var d0 any + if len(data) > 0 { + d0 = data[0] + } + + ret, err := localizer.Localize(&i18n.LocalizeConfig{ + DefaultMessage: &i18n.Message{ + ID: key, + Other: fallback, + }, + PluralCount: count, + TemplateData: d0, + }) + if err != nil { + fyne.LogError("Translation failure", err) + return fallbackWithData(key, fallback, d0) + } + return ret +} + +// AddTranslations allows an app to load a bundle of translations. +// The language that this relates to will be inferred from the resource name, for example "fr.json". +// The data should be in json format. +func AddTranslations(r fyne.Resource) error { + defer updateLocalizer() + return addLanguage(r.Content(), r.Name()) +} + +// AddTranslationsForLocale allows an app to load a bundle of translations for a specified locale. +// The data should be in json format. +func AddTranslationsForLocale(data []byte, l fyne.Locale) error { + defer updateLocalizer() + return addLanguage(data, l.String()+".json") +} + +// AddTranslationsFS supports adding all translations in one calling using an `embed.FS` setup. +// The `dir` parameter specifies the name or path of the directory containing translation files +// inside this embedded filesystem. +// Each file should be a json file with the name following pattern [prefix.]lang.json. +func AddTranslationsFS(fs embed.FS, dir string) (retErr error) { + files, err := fs.ReadDir(dir) + if err != nil { + return err + } + + for _, f := range files { + name := f.Name() + data, err := fs.ReadFile(dir + "/" + name) + if err != nil { + if retErr == nil { + retErr = err + } + continue + } + + err = addLanguage(data, name) + if err != nil { + if retErr == nil { + retErr = err + } + continue + } + } + + updateLocalizer() + + return retErr +} + +func addLanguage(data []byte, name string) error { + f, err := bundle.ParseMessageFileBytes(data, name) + if err != nil { + return err + } + + translated = append(translated, f.Tag) + return nil +} + +func init() { + bundle = i18n.NewBundle(language.English) + bundle.RegisterUnmarshalFunc("json", json.Unmarshal) + + translated = []language.Tag{language.Make("en")} // the first item in this list will be the fallback if none match + err := AddTranslationsFS(translations, "translations") + if err != nil { + fyne.LogError("Error occurred loading built-in translations", err) + } +} + +func fallbackWithData(key, fallback string, data any) string { + t, err := template.New(key).Parse(fallback) + if err != nil { + log.Println("Could not parse fallback template") + return fallback + } + str := &strings.Builder{} + _ = t.Execute(str, data) + return str.String() +} + +// A utility for setting up languages - available to unit tests for overriding system +func setupLang(lang string) { + localizer = i18n.NewLocalizer(bundle, lang) +} + +// updateLocalizer Finds the closest translation from the user's locale list and sets it up +func updateLocalizer() { + setupOnce.Do(initRuntime) + + all, err := locale.GetLocales() + if err != nil { + fyne.LogError("Failed to load user locales", err) + all = []string{"en"} + } + setupLang(closestSupportedLocale(all).LanguageString()) +} diff --git a/vendor/fyne.io/fyne/v2/lang/lang_android.go b/vendor/fyne.io/fyne/v2/lang/lang_android.go new file mode 100644 index 0000000..7c0f869 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/lang/lang_android.go @@ -0,0 +1,11 @@ +package lang + +import ( + "fyne.io/fyne/v2/internal/driver/mobile/app" + + "github.com/jeandeaual/go-locale" +) + +func initRuntime() { + locale.SetRunOnJVM(app.RunOnJVM) +} diff --git a/vendor/fyne.io/fyne/v2/lang/lang_notandroid.go b/vendor/fyne.io/fyne/v2/lang/lang_notandroid.go new file mode 100644 index 0000000..72537a1 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/lang/lang_notandroid.go @@ -0,0 +1,5 @@ +//go:build !android + +package lang + +func initRuntime() {} diff --git a/vendor/fyne.io/fyne/v2/lang/locale.go b/vendor/fyne.io/fyne/v2/lang/locale.go new file mode 100644 index 0000000..920cca1 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/lang/locale.go @@ -0,0 +1,56 @@ +package lang + +import ( + "github.com/jeandeaual/go-locale" + "golang.org/x/text/language" + + "fyne.io/fyne/v2" +) + +// SystemLocale returns the primary locale on the current system. +// This may refer to a language that Fyne does not have translations for. +func SystemLocale() fyne.Locale { + loc, err := locale.GetLocale() + if err != nil { + fyne.LogError("Failed to look up user locale", err) + } + if len(loc) < 2 { + loc = "en" + } + + tag, err := language.Parse(loc) + if err != nil { + fyne.LogError("Error parsing user locale "+loc, err) + } + return localeFromTag(tag) +} + +func closestSupportedLocale(locs []string) fyne.Locale { + matcher := language.NewMatcher(translated) + + tags := make([]language.Tag, len(locs)) + for i, loc := range locs { + tag, err := language.Parse(loc) + if err != nil { + fyne.LogError("Error parsing user locale "+loc, err) + } + tags[i] = tag + } + best, _, _ := matcher.Match(tags...) + return localeFromTag(best) +} + +func localeFromTag(in language.Tag) fyne.Locale { + b, s, r := in.Raw() + ret := b.String() + + if r.String() != "ZZ" { + ret += "-" + r.String() + + if s.String() != "Zzzz" { + ret += "-" + s.String() + } + } + + return fyne.Locale(ret) +} diff --git a/vendor/fyne.io/fyne/v2/lang/translations/base.cs.json b/vendor/fyne.io/fyne/v2/lang/translations/base.cs.json new file mode 100644 index 0000000..a1cb429 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/lang/translations/base.cs.json @@ -0,0 +1,45 @@ +{ + "Advanced": "Pokročilé", + "Cancel": "Zrušit", + "Confirm": "Potvrdit", + "Copy": "Kopírovat", + "Create Folder": "Vytvořit složku", + "Cut": "Vyjmout", + "Enter filename": "Zadejte název souboru", + "Error": "Chyba", + "Favourites": "Oblíbené", + "File": "Soubor", + "Folder": "Složka", + "New Folder": "Nová složka", + "No": "Ne", + "OK": "OK", + "Open": "Otevřít", + "Paste": "Vložit", + "Quit": "Odejít", + "Redo": "Opakovat", + "Save": "Uložit", + "Select all": "Vybrat vše", + "Show Hidden Files": "Zobrazit skryté soubory", + "Undo": "Zpět", + "Yes": "Ano", + "file.name": { + "other": "Název" + }, + "file.parent": { + "other": "Nadřazený" + }, + "friday": "Pátek", + "friday.short": "Pá", + "monday": "Pondělí", + "monday.short": "Po", + "saturday": "Sobota", + "saturday.short": "So", + "sunday": "Neděle", + "sunday.short": "Ne", + "thursday": "Čtvrtek", + "thursday.short": "Čt", + "tuesday": "Úterý", + "tuesday.short": "Út", + "wednesday": "Středa", + "wednesday.short": "St" +} diff --git a/vendor/fyne.io/fyne/v2/lang/translations/base.de.json b/vendor/fyne.io/fyne/v2/lang/translations/base.de.json new file mode 100644 index 0000000..6884c3f --- /dev/null +++ b/vendor/fyne.io/fyne/v2/lang/translations/base.de.json @@ -0,0 +1,45 @@ +{ + "Advanced": "Erweitert", + "Cancel": "Abbrechen", + "Confirm": "Bestätigen", + "Copy": "Kopieren", + "Create Folder": "Erstelle Ordner", + "Cut": "Ausschneiden", + "Enter filename": "Dateinamen eingeben", + "Error": "Fehler", + "Favourites": "Favoriten", + "File": "Datei", + "Folder": "Ordner", + "New Folder": "Neuer Ordner", + "No": "Nein", + "OK": "OK", + "Open": "Öffnen", + "Paste": "Einfügen", + "Quit": "Beenden", + "Redo": "Wiederholen", + "Save": "Speichern", + "Select all": "Alle auswählen", + "Show Hidden Files": "Versteckte Dateien anzeigen", + "Undo": "Rückgängig", + "Yes": "Ja", + "file.name": { + "other": "Name" + }, + "file.parent": { + "other": "../" + }, + "friday": "Freitag", + "friday.short": "Fr", + "monday": "Montag", + "monday.short": "Mo", + "saturday": "Samstag", + "saturday.short": "Sa", + "sunday": "Sonntag", + "sunday.short": "So", + "thursday": "Donnerstag", + "thursday.short": "Do", + "tuesday": "Dienstag", + "tuesday.short": "Di", + "wednesday": "Mittwoch", + "wednesday.short": "Mi" +} diff --git a/vendor/fyne.io/fyne/v2/lang/translations/base.el.json b/vendor/fyne.io/fyne/v2/lang/translations/base.el.json new file mode 100644 index 0000000..44390a4 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/lang/translations/base.el.json @@ -0,0 +1,45 @@ +{ + "Advanced": "Προχωρημένες", + "Cancel": "Ακύρωση", + "Confirm": "Επιβεβαίωση", + "Copy": "Αντιγραφή", + "Create Folder": "Δημιουργία Φακέλου", + "Cut": "Αποκοπή", + "Enter filename": "Εισαγωγή ονόματος αρχείου", + "Error": "Σφάλμα", + "Favourites": "Αγαπημένα", + "File": "Αρχείο", + "Folder": "Φάκελος", + "New Folder": "Νέος Φάκελος", + "No": "Όχι", + "OK": "ΟΚ", + "Open": "Άνοιγμα", + "Paste": "Επικόλληση", + "Quit": "Έξοδος", + "Redo": "Επανάληψη", + "Save": "Αποθήκευση", + "Select all": "Επιλογή όλων", + "Show Hidden Files": "Εμφάνιση Κρυμμένων Αρχείων", + "Undo": "Αναίρεση", + "Yes": "Ναι", + "file.name": { + "other": "Όνομα" + }, + "file.parent": { + "other": "Γονέας" + }, + "friday": "Παρασκευή", + "friday.short": "Παρ", + "monday": "Δευτέρα", + "monday.short": "Δευ", + "saturday": "Σαββάτο", + "saturday.short": "Σαβ", + "sunday": "Κυριακή", + "sunday.short": "Κυρ", + "thursday": "Πέμπτη", + "thursday.short": "Πεμ", + "tuesday": "Τρίτη", + "tuesday.short": "Τρι", + "wednesday": "Τετάρτη", + "wednesday.short": "Τερ" +} diff --git a/vendor/fyne.io/fyne/v2/lang/translations/base.en.json b/vendor/fyne.io/fyne/v2/lang/translations/base.en.json new file mode 100644 index 0000000..7670494 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/lang/translations/base.en.json @@ -0,0 +1,45 @@ +{ + "Advanced": "Advanced", + "Cancel": "Cancel", + "Confirm": "Confirm", + "Copy": "Copy", + "Create Folder": "Create Folder", + "Cut": "Cut", + "Enter filename": "Enter filename", + "Error": "Error", + "Favourites": "Favourites", + "File": "File", + "Folder": "Folder", + "New Folder": "New Folder", + "No": "No", + "OK": "OK", + "Open": "Open", + "Paste": "Paste", + "Quit": "Quit", + "Redo": "Redo", + "Save": "Save", + "Select all": "Select all", + "Show Hidden Files": "Show Hidden Files", + "Undo": "Undo", + "Yes": "Yes", + "file.name": { + "other": "Name" + }, + "file.parent": { + "other": "Parent" + }, + "friday": "Friday", + "friday.short": "Fri", + "monday": "Monday", + "monday.short": "Mon", + "saturday": "Saturday", + "saturday.short": "Sat", + "sunday": "Sunday", + "sunday.short": "Sun", + "thursday": "Thursday", + "thursday.short": "Thu", + "tuesday": "Tuesday", + "tuesday.short": "Tue", + "wednesday": "Wednesday", + "wednesday.short": "Wed" +} diff --git a/vendor/fyne.io/fyne/v2/lang/translations/base.es.json b/vendor/fyne.io/fyne/v2/lang/translations/base.es.json new file mode 100644 index 0000000..e960091 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/lang/translations/base.es.json @@ -0,0 +1,45 @@ +{ + "Advanced": "Avanzado", + "Cancel": "Cancelar", + "Confirm": "Confirmar", + "Copy": "Copiar", + "Create Folder": "Crear carpeta", + "Cut": "Cortar", + "Enter filename": "Introducir nombre del archivo", + "Error": "Error", + "Favourites": "Favoritos", + "File": "Archivo", + "Folder": "Carpeta", + "New Folder": "Nueva carpeta", + "No": "No", + "OK": "OK", + "Open": "Abrir", + "Paste": "Pegar", + "Quit": "Salir", + "Redo": "Rehacer", + "Save": "Guardar", + "Select all": "Seleccionar todo", + "Show Hidden Files": "Mostrar archivos ocultos", + "Undo": "Deshacer", + "Yes": "Si", + "file.name": { + "other": "Nombre" + }, + "file.parent": { + "other": "Padre" + }, + "monday": "Lunes", + "monday.short": "Lun", + "tuesday": "Martes", + "tuesday.short": "Mar", + "wednesday": "Miércoles", + "wednesday.short": "Mie", + "thursday": "Jueves", + "thursday.short": "Jue", + "friday": "Viernes", + "friday.short": "Vie", + "saturday": "Sábado", + "saturday.short": "Sab", + "sunday": "Domingo", + "sunday.short": "Dom" +} diff --git a/vendor/fyne.io/fyne/v2/lang/translations/base.fr.json b/vendor/fyne.io/fyne/v2/lang/translations/base.fr.json new file mode 100644 index 0000000..2134ad0 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/lang/translations/base.fr.json @@ -0,0 +1,31 @@ +{ + "Quit": "Quitter", + "Create Folder": "Créer un dossier", + "Cut": "Couper", + "Folder": "Dossier", + "Advanced": "Avancé", + "Cancel": "Annuler", + "Confirm": "Confirmer", + "Copy": "Copier", + "Enter filename": "Entrez un nom de fichier", + "Error": "Erreur", + "Favourites": "Favoris", + "File": "Fichier", + "New Folder": "Nouveau dossier", + "No": "Non", + "OK": "OK", + "Open": "Ouvrir", + "Paste": "Coller", + "Redo": "Rétablir", + "Save": "Enregistrer", + "Select all": "Tout sélectionner", + "Show Hidden Files": "Afficher les fichiers cachés", + "Undo": "Annuler", + "Yes": "Oui", + "file.name": { + "other": "Nom" + }, + "file.parent": { + "other": "Parent" + } +} diff --git a/vendor/fyne.io/fyne/v2/lang/translations/base.pl.json b/vendor/fyne.io/fyne/v2/lang/translations/base.pl.json new file mode 100644 index 0000000..8d8140e --- /dev/null +++ b/vendor/fyne.io/fyne/v2/lang/translations/base.pl.json @@ -0,0 +1,31 @@ +{ + "Advanced": "Zaawansowane", + "Cancel": "Anuluj", + "Confirm": "Potwierdz", + "Copy": "Kopiuj", + "Cut": "Wytnij", + "Error": "Błąd", + "Favourites": "Ulubione", + "File": "Plik", + "New Folder": "Nowy folder", + "No": "Nie", + "OK": "OK", + "Open": "Otwórz", + "Paste": "Wklej", + "Quit": "Wyjdz", + "Redo": "Przerób", + "Save": "Zapisz", + "Show Hidden Files": "Pokaż Ukryte Pliki", + "Enter filename": "Wprowadź Nazwę Pliku", + "Select all": "Zaznacz Wszystko", + "Yes": "Tak", + "file.name": { + "other": "Nawza" + }, + "Folder": "Folder", + "Create Folder": "Utwórz Folder", + "Undo": "Cofnij", + "file.parent": { + "other": "Rodzic" + } +} diff --git a/vendor/fyne.io/fyne/v2/lang/translations/base.pt.json b/vendor/fyne.io/fyne/v2/lang/translations/base.pt.json new file mode 100644 index 0000000..55599e0 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/lang/translations/base.pt.json @@ -0,0 +1,45 @@ +{ + "Advanced": "Avançado", + "Cancel": "Cancelar", + "Confirm": "Confirmar", + "Copy": "Copiar", + "Create Folder": "Criar Pasta", + "Cut": "Recortar", + "Enter filename": "Digite o nome do ficheiro", + "Error": "Erro", + "Favourites": "Favoritos", + "File": "Ficheiro", + "Folder": "Pasta", + "New Folder": "Pasta nova", + "No": "Não", + "OK": "OK", + "Open": "Abrir", + "Paste": "Colar", + "Quit": "Sair", + "Redo": "Refazer", + "Save": "Gravar", + "Select all": "Selecionar tudo", + "Show Hidden Files": "Mostrar ficheiros escondidos", + "Undo": "Desfazer", + "Yes": "Sim", + "file.name": { + "other": "Nome" + }, + "file.parent": { + "other": "Origem" + }, + "friday": "Sexta", + "friday.short": "Sex", + "monday": "Segunda", + "monday.short": "Seg", + "saturday": "Sábado", + "saturday.short": "Sáb", + "sunday": "Domingo", + "sunday.short": "Dom", + "thursday": "Quinta", + "thursday.short": "Qui", + "tuesday": "Terça", + "tuesday.short": "Ter", + "wednesday": "Quarta", + "wednesday.short": "Qua" +} diff --git a/vendor/fyne.io/fyne/v2/lang/translations/base.pt_BR.json b/vendor/fyne.io/fyne/v2/lang/translations/base.pt_BR.json new file mode 100644 index 0000000..4e532b3 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/lang/translations/base.pt_BR.json @@ -0,0 +1,31 @@ +{ + "Advanced": "Avançado", + "Cancel": "Cancelar", + "Confirm": "Confirmar", + "Copy": "Copiar", + "Create Folder": "Criar Pasta", + "Cut": "Recortar", + "Enter filename": "Digite o nome do arquivo", + "Error": "Erro", + "Favourites": "Favoritos", + "File": "Arquivo", + "Folder": "Pasta", + "New Folder": "Nova Pasta", + "No": "Não", + "OK": "OK", + "Open": "Abrir", + "Paste": "Colar", + "Quit": "Sair", + "Redo": "Refazer", + "Save": "Salvar", + "Select all": "Selecionar tudo", + "Show Hidden Files": "Mostrar Arquivos Ocultos", + "Undo": "Desfazer", + "Yes": "Sim", + "file.name": { + "other": "Nome" + }, + "file.parent": { + "other": "Origem" + } +} diff --git a/vendor/fyne.io/fyne/v2/lang/translations/base.ru.json b/vendor/fyne.io/fyne/v2/lang/translations/base.ru.json new file mode 100644 index 0000000..137e544 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/lang/translations/base.ru.json @@ -0,0 +1,45 @@ +{ + "Advanced": "Расширенные", + "Cancel": "Отмена", + "Confirm": "Подтвердить", + "Copy": "Копировать", + "Create Folder": "Создать папку", + "Cut": "Вырезать", + "Enter filename": "Введите имя файла", + "Error": "Ошибка", + "Favourites": "Избранное", + "File": "Файл", + "Folder": "Папка", + "New Folder": "Новая папка", + "No": "Нет", + "OK": "ОК", + "Open": "Открыть", + "Paste": "Вставить", + "Quit": "Выйти", + "Redo": "Повторить", + "Save": "Сохранить", + "Select all": "Выбрать всё", + "Show Hidden Files": "Показать скрытые файлы", + "Undo": "Отменить", + "Yes": "Да", + "file.name": { + "other": "Имя" + }, + "file.parent": { + "other": "Вверх" + }, + "friday": "Пятница", + "friday.short": "Пт", + "monday": "Понедельник", + "monday.short": "Пн", + "saturday": "Суббота", + "saturday.short": "Сб", + "sunday": "Воскресенье", + "sunday.short": "Вс", + "thursday": "Вторник", + "thursday.short": "Вт", + "tuesday": "Четверг", + "tuesday.short": "Чт", + "wednesday": "Среда", + "wednesday.short": "Ср" +} diff --git a/vendor/fyne.io/fyne/v2/lang/translations/base.sv.json b/vendor/fyne.io/fyne/v2/lang/translations/base.sv.json new file mode 100644 index 0000000..6979560 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/lang/translations/base.sv.json @@ -0,0 +1,45 @@ +{ + "OK": "OK", + "Save": "Spara", + "Advanced": "Avancerad", + "Confirm": "Bekräfta", + "Error": "Fel", + "Cancel": "Avbryt", + "Copy": "Kopiera", + "Create Folder": "Skapa mapp", + "Cut": "Klipp ut", + "Enter filename": "Ange filnamn", + "Favourites": "Favoriter", + "File": "Fil", + "Folder": "Mapp", + "New Folder": "Ny mapp", + "Open": "Öppna", + "Select all": "Markera allt", + "Undo": "Ångra", + "Yes": "Ja", + "file.name": { + "other": "Namn" + }, + "Show Hidden Files": "Visa dolda filer", + "thursday": "Torsdag", + "thursday.short": "Tor", + "friday": "Fredag", + "friday.short": "Fre", + "saturday": "Lördag", + "saturday.short": "Lör", + "sunday": "Söndag", + "sunday.short": "Sön", + "No": "Nej", + "Paste": "Klistra in", + "Quit": "Avsluta", + "Redo": "Gör om", + "monday": "Måndag", + "monday.short": "Mån", + "file.parent": { + "other": "Överordnad" + }, + "tuesday": "Tisdag", + "tuesday.short": "Tis", + "wednesday": "Onsdag", + "wednesday.short": "Ons" +} diff --git a/vendor/fyne.io/fyne/v2/lang/translations/base.ta.json b/vendor/fyne.io/fyne/v2/lang/translations/base.ta.json new file mode 100644 index 0000000..e8ba402 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/lang/translations/base.ta.json @@ -0,0 +1,45 @@ +{ + "Advanced": "மேம்பட்ட", + "Cancel": "ரத்துசெய்", + "Confirm": "உறுதிப்படுத்தவும்", + "Copy": "நகலெடு", + "Create Folder": "கோப்புறையை உருவாக்கவும்", + "Cut": "வெட்டு", + "Enter filename": "கோப்பு பெயரை உள்ளிடவும்", + "Error": "பிழை", + "Favourites": "பிடித்தவை", + "File": "கோப்பு", + "Folder": "கோப்புறை", + "New Folder": "புதிய கோப்புறை", + "No": "இல்லை", + "OK": "சரி", + "Open": "திற", + "Paste": "ஒட்டு", + "Quit": "வெளியேறு", + "Redo": "மீண்டும்செய்", + "Save": "சேமி", + "Select all": "அனைத்தையும் தெரிவுசெய்", + "Show Hidden Files": "மறைக்கப்பட்ட கோப்புகளைக் காட்டு", + "Undo": "செயல்தவிர்", + "Yes": "ஆம்", + "file.name": { + "other": "பெயர்" + }, + "file.parent": { + "other": "பெற்றோர்" + }, + "friday": "வெள்ளிக்கிழமை", + "friday.short": "வெள்ளி", + "monday": "திங்கள்", + "monday.short": "தி", + "saturday": "காரிக்கிழமை", + "saturday.short": "காரி", + "sunday": "ஞாயிற்றுக்கிழமை", + "sunday.short": "சூரியன்", + "thursday": "வியாழக்கிழமை", + "thursday.short": "வியாழன்", + "tuesday": "செவ்வாய்க்கிழமை", + "tuesday.short": "செவ்வாய்", + "wednesday": "புதன்கிழமை", + "wednesday.short": "அறிவன்" +} diff --git a/vendor/fyne.io/fyne/v2/lang/translations/base.uk.json b/vendor/fyne.io/fyne/v2/lang/translations/base.uk.json new file mode 100644 index 0000000..4af4368 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/lang/translations/base.uk.json @@ -0,0 +1,45 @@ +{ + "Advanced": "Розширені", + "Cancel": "Скасувати", + "Confirm": "Підтвердити", + "Copy": "Скопіювати", + "Create Folder": "Створити теку", + "Cut": "Вирізати", + "Enter filename": "Введіть назву теки", + "Error": "Помилка", + "Favourites": "Обране", + "File": "Файл", + "Folder": "Тека", + "New Folder": "Нова тека", + "No": "Ні", + "OK": "Добре", + "Open": "Відкрити", + "Paste": "Вставити", + "Quit": "Вийти", + "Redo": "Повторити", + "Save": "Зберегти", + "Select all": "Обрати все", + "Show Hidden Files": "Показувати приховані файли", + "Undo": "Відмінити", + "Yes": "Так", + "file.name": { + "other": "Назва" + }, + "file.parent": { + "other": "Батьківська" + }, + "friday": "П'ятниця", + "friday.short": "П'ят", + "monday": "Понеділок", + "monday.short": "Пон", + "saturday": "Субота", + "saturday.short": "Суб", + "sunday": "Неділя", + "sunday.short": "Нед", + "thursday": "Четвер", + "thursday.short": "Чет", + "tuesday": "Вівторок", + "tuesday.short": "Віт", + "wednesday": "Середа", + "wednesday.short": "Сер" +} diff --git a/vendor/fyne.io/fyne/v2/lang/translations/base.zh_Hans.json b/vendor/fyne.io/fyne/v2/lang/translations/base.zh_Hans.json new file mode 100644 index 0000000..0519f81 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/lang/translations/base.zh_Hans.json @@ -0,0 +1,45 @@ +{ + "Advanced": "高级", + "Cancel": "取消", + "Confirm": "确认", + "Copy": "复制", + "Create Folder": "新建文件夹", + "Cut": "剪切", + "Enter filename": "输入文件名", + "Error": "错误", + "Favourites": "收藏", + "File": "文件", + "Folder": "文件夹", + "New Folder": "新文件夹", + "No": "不", + "OK": "好", + "Open": "打开", + "Paste": "粘贴", + "Quit": "退出", + "Redo": "重做", + "Save": "保存", + "Select all": "选择全部", + "Show Hidden Files": "显示隐藏的文件", + "Undo": "撤消", + "Yes": "是", + "file.name": { + "other": "名字" + }, + "file.parent": { + "other": "父目录" + }, + "friday": "星期五", + "friday.short": "周五", + "monday": "星期一", + "monday.short": "周一", + "saturday": "星期六", + "saturday.short": "周六", + "sunday": "星期日", + "sunday.short": "周日", + "thursday": "星期四", + "thursday.short": "周四", + "tuesday": "星期二", + "tuesday.short": "周二", + "wednesday": "星期三", + "wednesday.short": "周三" +} diff --git a/vendor/fyne.io/fyne/v2/layout.go b/vendor/fyne.io/fyne/v2/layout.go new file mode 100644 index 0000000..8a3d804 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/layout.go @@ -0,0 +1,11 @@ +package fyne + +// Layout defines how [CanvasObject]s may be laid out in a specified Size. +type Layout interface { + // Layout will manipulate the listed [CanvasObject]s Size and Position + // to fit within the specified size. + Layout([]CanvasObject, Size) + // MinSize calculates the smallest size that will fit the listed + // [CanvasObject]s using this Layout algorithm. + MinSize(objects []CanvasObject) Size +} diff --git a/vendor/fyne.io/fyne/v2/layout/borderlayout.go b/vendor/fyne.io/fyne/v2/layout/borderlayout.go new file mode 100644 index 0000000..2cc420c --- /dev/null +++ b/vendor/fyne.io/fyne/v2/layout/borderlayout.go @@ -0,0 +1,108 @@ +package layout + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/theme" +) + +// Declare conformity with Layout interface +var _ fyne.Layout = (*borderLayout)(nil) + +type borderLayout struct { + top, bottom, left, right fyne.CanvasObject +} + +// NewBorderLayout creates a new BorderLayout instance with top, bottom, left +// and right objects set. All other items in the container will fill the remaining space in the middle. +// Multiple extra items will be stacked in the specified order as a Stack container. +func NewBorderLayout(top, bottom, left, right fyne.CanvasObject) fyne.Layout { + return &borderLayout{top, bottom, left, right} +} + +// Layout is called to pack all child objects into a specified size. +// For BorderLayout this arranges the top, bottom, left and right widgets at +// the sides and any remaining widgets are maximised in the middle space. +func (b *borderLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) { + padding := theme.Padding() + var topSize, bottomSize, leftSize, rightSize fyne.Size + if b.top != nil && b.top.Visible() { + topHeight := b.top.MinSize().Height + b.top.Resize(fyne.NewSize(size.Width, topHeight)) + b.top.Move(fyne.NewPos(0, 0)) + topSize = fyne.NewSize(size.Width, topHeight+padding) + } + if b.bottom != nil && b.bottom.Visible() { + bottomHeight := b.bottom.MinSize().Height + b.bottom.Resize(fyne.NewSize(size.Width, bottomHeight)) + b.bottom.Move(fyne.NewPos(0, size.Height-bottomHeight)) + bottomSize = fyne.NewSize(size.Width, bottomHeight+padding) + } + if b.left != nil && b.left.Visible() { + leftWidth := b.left.MinSize().Width + b.left.Resize(fyne.NewSize(leftWidth, size.Height-topSize.Height-bottomSize.Height)) + b.left.Move(fyne.NewPos(0, topSize.Height)) + leftSize = fyne.NewSize(leftWidth+padding, size.Height-topSize.Height-bottomSize.Height) + } + if b.right != nil && b.right.Visible() { + rightWidth := b.right.MinSize().Width + b.right.Resize(fyne.NewSize(rightWidth, size.Height-topSize.Height-bottomSize.Height)) + b.right.Move(fyne.NewPos(size.Width-rightWidth, topSize.Height)) + rightSize = fyne.NewSize(rightWidth+padding, size.Height-topSize.Height-bottomSize.Height) + } + + middleSize := fyne.NewSize(size.Width-leftSize.Width-rightSize.Width, size.Height-topSize.Height-bottomSize.Height) + middlePos := fyne.NewPos(leftSize.Width, topSize.Height) + for _, child := range objects { + if !child.Visible() { + continue + } + + if child != b.top && child != b.bottom && child != b.left && child != b.right { + child.Resize(middleSize) + child.Move(middlePos) + } + } +} + +// MinSize finds the smallest size that satisfies all the child objects. +// For BorderLayout this is determined by the MinSize height of the top and +// plus the MinSize width of the left and right, plus any padding needed. +// This is then added to the union of the MinSize for any remaining content. +func (b *borderLayout) MinSize(objects []fyne.CanvasObject) fyne.Size { + minSize := fyne.NewSize(0, 0) + for _, child := range objects { + if !child.Visible() { + continue + } + + if child != b.top && child != b.bottom && child != b.left && child != b.right { + minSize = minSize.Max(child.MinSize()) + } + } + + padding := theme.Padding() + + if b.left != nil && b.left.Visible() { + leftMin := b.left.MinSize() + minHeight := fyne.Max(minSize.Height, leftMin.Height) + minSize = fyne.NewSize(minSize.Width+leftMin.Width+padding, minHeight) + } + if b.right != nil && b.right.Visible() { + rightMin := b.right.MinSize() + minHeight := fyne.Max(minSize.Height, rightMin.Height) + minSize = fyne.NewSize(minSize.Width+rightMin.Width+padding, minHeight) + } + + if b.top != nil && b.top.Visible() { + topMin := b.top.MinSize() + minWidth := fyne.Max(minSize.Width, topMin.Width) + minSize = fyne.NewSize(minWidth, minSize.Height+topMin.Height+padding) + } + if b.bottom != nil && b.bottom.Visible() { + bottomMin := b.bottom.MinSize() + minWidth := fyne.Max(minSize.Width, bottomMin.Width) + minSize = fyne.NewSize(minWidth, minSize.Height+bottomMin.Height+padding) + } + + return minSize +} diff --git a/vendor/fyne.io/fyne/v2/layout/boxlayout.go b/vendor/fyne.io/fyne/v2/layout/boxlayout.go new file mode 100644 index 0000000..bd00e5f --- /dev/null +++ b/vendor/fyne.io/fyne/v2/layout/boxlayout.go @@ -0,0 +1,227 @@ +package layout + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/theme" +) + +// NewVBoxLayout returns a vertical box layout for stacking a number of child +// canvas objects or widgets top to bottom. The objects are always displayed +// at their vertical MinSize. Use a different layout if the objects are intended +// to be larger than their vertical MinSize. +func NewVBoxLayout() fyne.Layout { + return vBoxLayout{ + paddingFunc: theme.Padding, + } +} + +// NewHBoxLayout returns a horizontal box layout for stacking a number of child +// canvas objects or widgets left to right. The objects are always displayed +// at their horizontal MinSize. Use a different layout if the objects are intended +// to be larger than their horizontal MinSize. +func NewHBoxLayout() fyne.Layout { + return hBoxLayout{ + paddingFunc: theme.Padding, + } +} + +// NewCustomPaddedHBoxLayout returns a layout similar to HBoxLayout that uses a custom +// amount of padding in between objects instead of the theme.Padding value. +// +// Since: 2.5 +func NewCustomPaddedHBoxLayout(padding float32) fyne.Layout { + return hBoxLayout{ + paddingFunc: func() float32 { return padding }, + } +} + +// NewCustomPaddedVBoxLayout returns a layout similar to VBoxLayout that uses a custom +// amount of padding in between objects instead of the theme.Padding value. +// +// Since: 2.5 +func NewCustomPaddedVBoxLayout(padding float32) fyne.Layout { + return vBoxLayout{ + paddingFunc: func() float32 { return padding }, + } +} + +// Declare conformity with Layout interface +var _ fyne.Layout = (*vBoxLayout)(nil) + +type vBoxLayout struct { + paddingFunc func() float32 +} + +// Layout is called to pack all child objects into a specified size. +// This will pack objects into a single column where each item +// is full width but the height is the minimum required. +// Any spacers added will pad the view, sharing the space if there are two or more. +func (v vBoxLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) { + spacers := 0 + visibleObjects := 0 + // Size taken up by visible objects + total := float32(0) + + for _, child := range objects { + if !child.Visible() { + continue + } + + if isVerticalSpacer(child) { + spacers++ + continue + } + + visibleObjects++ + total += child.MinSize().Height + } + + padding := v.paddingFunc() + + // Amount of space not taken up by visible objects and inter-object padding + extra := size.Height - total - (padding * float32(visibleObjects-1)) + + // Spacers split extra space equally + spacerSize := float32(0) + if spacers > 0 { + spacerSize = extra / float32(spacers) + } + + x, y := float32(0), float32(0) + for _, child := range objects { + if !child.Visible() { + continue + } + + if isVerticalSpacer(child) { + child.Move(fyne.NewPos(x, y)) + child.Resize(fyne.NewSize(size.Width, spacerSize)) + y += spacerSize + continue + } + child.Move(fyne.NewPos(x, y)) + + height := child.MinSize().Height + y += padding + height + child.Resize(fyne.NewSize(size.Width, height)) + } +} + +// MinSize finds the smallest size that satisfies all the child objects. +// For a BoxLayout this is the width of the widest item and the height is +// the sum of all children combined with padding between each. +func (v vBoxLayout) MinSize(objects []fyne.CanvasObject) fyne.Size { + minSize := fyne.NewSize(0, 0) + addPadding := false + padding := v.paddingFunc() + for _, child := range objects { + if !child.Visible() || isVerticalSpacer(child) { + continue + } + + childMin := child.MinSize() + minSize.Width = fyne.Max(childMin.Width, minSize.Width) + minSize.Height += childMin.Height + if addPadding { + minSize.Height += padding + } + addPadding = true + } + return minSize +} + +// Declare conformity with Layout interface +var _ fyne.Layout = (*hBoxLayout)(nil) + +type hBoxLayout struct { + paddingFunc func() float32 +} + +// Layout is called to pack all child objects into a specified size. +// For a VBoxLayout this will pack objects into a single column where each item +// is full width but the height is the minimum required. +// Any spacers added will pad the view, sharing the space if there are two or more. +func (g hBoxLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) { + spacers := 0 + visibleObjects := 0 + // Size taken up by visible objects + total := float32(0) + + for _, child := range objects { + if !child.Visible() { + continue + } + + if isHorizontalSpacer(child) { + spacers++ + continue + } + + visibleObjects++ + total += child.MinSize().Width + } + + padding := g.paddingFunc() + + // Amount of space not taken up by visible objects and inter-object padding + extra := size.Width - total - (padding * float32(visibleObjects-1)) + + // Spacers split extra space equally + spacerSize := float32(0) + if spacers > 0 { + spacerSize = extra / float32(spacers) + } + + x, y := float32(0), float32(0) + for _, child := range objects { + if !child.Visible() { + continue + } + + if isHorizontalSpacer(child) { + child.Move(fyne.NewPos(x, y)) + child.Resize(fyne.NewSize(spacerSize, size.Height)) + + x += spacerSize + continue + } + child.Move(fyne.NewPos(x, y)) + + width := child.MinSize().Width + x += padding + width + child.Resize(fyne.NewSize(width, size.Height)) + } +} + +// MinSize finds the smallest size that satisfies all the child objects. +// For a BoxLayout this is the width of the widest item and the height is +// the sum of all children combined with padding between each. +func (g hBoxLayout) MinSize(objects []fyne.CanvasObject) fyne.Size { + minSize := fyne.NewSize(0, 0) + addPadding := false + padding := g.paddingFunc() + for _, child := range objects { + if !child.Visible() || isHorizontalSpacer(child) { + continue + } + + childMin := child.MinSize() + minSize.Height = fyne.Max(childMin.Height, minSize.Height) + minSize.Width += childMin.Width + if addPadding { + minSize.Width += padding + } + addPadding = true + } + return minSize +} + +func isVerticalSpacer(obj fyne.CanvasObject) bool { + spacer, ok := obj.(SpacerObject) + return ok && spacer.ExpandVertical() +} + +func isHorizontalSpacer(obj fyne.CanvasObject) bool { + spacer, ok := obj.(SpacerObject) + return ok && spacer.ExpandHorizontal() +} diff --git a/vendor/fyne.io/fyne/v2/layout/centerlayout.go b/vendor/fyne.io/fyne/v2/layout/centerlayout.go new file mode 100644 index 0000000..26766ce --- /dev/null +++ b/vendor/fyne.io/fyne/v2/layout/centerlayout.go @@ -0,0 +1,38 @@ +package layout + +import "fyne.io/fyne/v2" + +// Declare conformity with Layout interface +var _ fyne.Layout = (*centerLayout)(nil) + +type centerLayout struct{} + +// NewCenterLayout creates a new CenterLayout instance +func NewCenterLayout() fyne.Layout { + return ¢erLayout{} +} + +// Layout is called to pack all child objects into a specified size. +// For CenterLayout this sets all children to their minimum size, centered within the space. +func (c *centerLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) { + for _, child := range objects { + childMin := child.MinSize() + child.Resize(childMin) + child.Move(fyne.NewPos((size.Width-childMin.Width)/2, (size.Height-childMin.Height)/2)) + } +} + +// MinSize finds the smallest size that satisfies all the child objects. +// For CenterLayout this is determined simply as the MinSize of the largest child. +func (c *centerLayout) MinSize(objects []fyne.CanvasObject) fyne.Size { + minSize := fyne.NewSize(0, 0) + for _, child := range objects { + if !child.Visible() { + continue + } + + minSize = minSize.Max(child.MinSize()) + } + + return minSize +} diff --git a/vendor/fyne.io/fyne/v2/layout/custompaddedlayout.go b/vendor/fyne.io/fyne/v2/layout/custompaddedlayout.go new file mode 100644 index 0000000..b896994 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/layout/custompaddedlayout.go @@ -0,0 +1,61 @@ +package layout + +import ( + "fyne.io/fyne/v2" +) + +// Declare conformity with Layout interface +var _ fyne.Layout = (*CustomPaddedLayout)(nil) + +// CustomPaddedLayout is a layout similar to PaddedLayout, but uses +// custom values for padding on each side, rather than the theme padding value. +// +// Since: 2.5 +type CustomPaddedLayout struct { + TopPadding float32 + BottomPadding float32 + LeftPadding float32 + RightPadding float32 +} + +// NewCustomPaddedLayout creates a new CustomPaddedLayout instance +// with the specified paddings. +// +// Since: 2.5 +func NewCustomPaddedLayout(padTop, padBottom, padLeft, padRight float32) fyne.Layout { + return CustomPaddedLayout{ + TopPadding: padTop, + BottomPadding: padBottom, + LeftPadding: padLeft, + RightPadding: padRight, + } +} + +// Layout is called to pack all child objects into a specified size. +// For CustomPaddedLayout this sets all children to the full size passed minus the given paddings all around. +func (c CustomPaddedLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) { + pos := fyne.NewPos(c.LeftPadding, c.TopPadding) + siz := fyne.Size{ + Width: size.Width - c.LeftPadding - c.RightPadding, + Height: size.Height - c.TopPadding - c.BottomPadding, + } + for _, child := range objects { + child.Resize(siz) + child.Move(pos) + } +} + +// MinSize finds the smallest size that satisfies all the child objects. +// For CustomPaddedLayout this is determined simply as the MinSize of the largest child plus the given paddings all around. +func (c CustomPaddedLayout) MinSize(objects []fyne.CanvasObject) (min fyne.Size) { + for _, child := range objects { + if !child.Visible() { + continue + } + + min = min.Max(child.MinSize()) + } + min.Width += c.LeftPadding + c.RightPadding + min.Height += c.TopPadding + c.BottomPadding + return min +} diff --git a/vendor/fyne.io/fyne/v2/layout/formlayout.go b/vendor/fyne.io/fyne/v2/layout/formlayout.go new file mode 100644 index 0000000..6e8a5ea --- /dev/null +++ b/vendor/fyne.io/fyne/v2/layout/formlayout.go @@ -0,0 +1,133 @@ +package layout + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/theme" +) + +const formLayoutCols = 2 + +// Declare conformity with Layout interface +var _ fyne.Layout = (*formLayout)(nil) + +// formLayout is two column grid where each row has a label and a widget. +type formLayout struct{} + +// calculateTableSizes calculates the izes of the table. +// This includes the width of the label column (maximum width of all labels), +// the width of the content column (maximum width of all content cells and remaining space in container) +// and the total minimum height of the form. +func (f *formLayout) calculateTableSizes(objects []fyne.CanvasObject, containerWidth float32) (labelWidth float32, contentWidth float32, height float32) { + if len(objects)%formLayoutCols != 0 { + return 0, 0, 0 + } + + rows := 0 + innerPadding := theme.InnerPadding() + for i := 0; i < len(objects); i += formLayoutCols { + labelCell, contentCell := objects[i], objects[i+1] + if !labelCell.Visible() && !contentCell.Visible() { + continue + } + + // Label column width is the maximum of all labels. + labelSize := labelCell.MinSize() + if _, ok := labelCell.(*canvas.Text); ok { + labelSize.Width += innerPadding * 2 + labelSize.Height += innerPadding * 2 + } + labelWidth = fyne.Max(labelWidth, labelSize.Width) + + // Content column width is the maximum of all content items. + contentSize := contentCell.MinSize() + if _, ok := contentCell.(*canvas.Text); ok { + contentSize.Width += innerPadding * 2 + contentSize.Height += innerPadding * 2 + } + contentWidth = fyne.Max(contentWidth, contentSize.Width) + + rowHeight := fyne.Max(labelSize.Height, contentSize.Height) + height += rowHeight + rows++ + } + + padding := theme.Padding() + contentWidth = fyne.Max(contentWidth, containerWidth-labelWidth-padding) + return labelWidth, contentWidth, height + float32(rows-1)*padding +} + +// Layout is called to pack all child objects into a table format with two columns. +func (f *formLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) { + labelWidth, contentWidth, _ := f.calculateTableSizes(objects, size.Width) + + y := float32(0) + padding := theme.Padding() + innerPadding := theme.InnerPadding() + + // Calculate size and position of object. Position and size is returned (instead of calling Move() and Resize()) to make inlineable. + objectLayout := func(obj fyne.CanvasObject, offset, width, rowHeight, itemHeight float32) (fyne.Position, fyne.Size) { + pos := fyne.NewPos(offset, y) + size := fyne.NewSize(width, rowHeight) + if _, ok := obj.(*canvas.Text); ok { + pos = pos.AddXY(innerPadding, innerPadding) + size.Width -= innerPadding * 2 + size.Height = itemHeight + } + + return pos, size + } + + remainder := len(objects) % formLayoutCols + for i := 0; i < len(objects)-remainder; i += formLayoutCols { + labelCell, contentCell := objects[i], objects[i+1] + if !labelCell.Visible() && !contentCell.Visible() { + continue + } + + labelMin := labelCell.MinSize() + contentMin := contentCell.MinSize() + labelHeight := labelMin.Height + contentHeight := contentMin.Height + if _, ok := labelCell.(*canvas.Text); ok { + labelHeight += innerPadding * 2 + } + if _, ok := contentCell.(*canvas.Text); ok { + contentHeight += innerPadding * 2 + } + rowHeight := fyne.Max(labelHeight, contentHeight) + + pos, size := objectLayout(labelCell, 0, labelWidth, rowHeight, labelMin.Height) + labelCell.Move(pos) + labelCell.Resize(size) + + pos, size = objectLayout(contentCell, labelWidth+padding, contentWidth, rowHeight, contentMin.Height) + contentCell.Move(pos) + contentCell.Resize(size) + + y += rowHeight + padding + } + + // Handle remaining item in the case of uneven number of objects: + if remainder == 1 { + lastCell := objects[len(objects)-1] + lastMin := lastCell.MinSize() + objectLayout(lastCell, 0, labelWidth, lastMin.Height, lastMin.Height) + } +} + +// MinSize finds the smallest size that satisfies all the child objects. +// For a FormLayout this is the width of the widest label and content items and the height is +// the sum of all column children combined with padding between each. +func (f *formLayout) MinSize(objects []fyne.CanvasObject) fyne.Size { + labelWidth, contentWidth, height := f.calculateTableSizes(objects, 0) + return fyne.Size{ + Width: labelWidth + contentWidth + theme.Padding(), + Height: height, + } +} + +// NewFormLayout returns a new FormLayout instance +func NewFormLayout() fyne.Layout { + return &formLayout{} +} diff --git a/vendor/fyne.io/fyne/v2/layout/gridlayout.go b/vendor/fyne.io/fyne/v2/layout/gridlayout.go new file mode 100644 index 0000000..316b2ed --- /dev/null +++ b/vendor/fyne.io/fyne/v2/layout/gridlayout.go @@ -0,0 +1,156 @@ +package layout + +import ( + "math" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/theme" +) + +// Declare conformity with Layout interface +var _ fyne.Layout = (*gridLayout)(nil) + +type gridLayout struct { + Cols int + vertical, adapt bool +} + +// NewAdaptiveGridLayout returns a new grid layout which uses columns when horizontal but rows when vertical. +func NewAdaptiveGridLayout(rowcols int) fyne.Layout { + return &gridLayout{Cols: rowcols, adapt: true} +} + +// NewGridLayout returns a grid layout arranged in a specified number of columns. +// The number of rows will depend on how many children are in the container that uses this layout. +func NewGridLayout(cols int) fyne.Layout { + return NewGridLayoutWithColumns(cols) +} + +// NewGridLayoutWithColumns returns a new grid layout that specifies a column count and wrap to new rows when needed. +func NewGridLayoutWithColumns(cols int) fyne.Layout { + return &gridLayout{Cols: cols} +} + +// NewGridLayoutWithRows returns a new grid layout that specifies a row count that creates new rows as required. +func NewGridLayoutWithRows(rows int) fyne.Layout { + return &gridLayout{Cols: rows, vertical: true} +} + +func (g *gridLayout) horizontal() bool { + if g.adapt { + return fyne.IsHorizontal(fyne.CurrentDevice().Orientation()) + } + + return !g.vertical +} + +func (g *gridLayout) countRows(objects []fyne.CanvasObject) int { + if g.Cols < 1 { + g.Cols = 1 + } + count := 0 + for _, child := range objects { + if child.Visible() { + count++ + } + } + + return int(math.Ceil(float64(count) / float64(g.Cols))) +} + +// Get the leading (top or left) edge of a grid cell. +// size is the ideal cell size and the offset is which col or row its on. +func getLeading(size float64, offset int) float32 { + ret := (size + float64(theme.Padding())) * float64(offset) + return float32(ret) +} + +// Get the trailing (bottom or right) edge of a grid cell. +// size is the ideal cell size and the offset is which col or row its on. +func getTrailing(size float64, offset int) float32 { + return getLeading(size, offset+1) - theme.Padding() +} + +// Layout is called to pack all child objects into a specified size. +// For a GridLayout this will pack objects into a table format with the number +// of columns specified in our constructor. +func (g *gridLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) { + rows := g.countRows(objects) + + padding := theme.Padding() + + primaryObjects := rows + secondaryObjects := g.Cols + if g.horizontal() { + primaryObjects, secondaryObjects = secondaryObjects, primaryObjects + } + + padWidth := float32(primaryObjects-1) * padding + padHeight := float32(secondaryObjects-1) * padding + cellWidth := float64(size.Width-padWidth) / float64(primaryObjects) + cellHeight := float64(size.Height-padHeight) / float64(secondaryObjects) + + row, col := 0, 0 + i := 0 + for _, child := range objects { + if !child.Visible() { + continue + } + + x1 := getLeading(cellWidth, col) + y1 := getLeading(cellHeight, row) + x2 := getTrailing(cellWidth, col) + y2 := getTrailing(cellHeight, row) + + child.Move(fyne.NewPos(x1, y1)) + child.Resize(fyne.NewSize(x2-x1, y2-y1)) + + if g.horizontal() { + if (i+1)%g.Cols == 0 { + row++ + col = 0 + } else { + col++ + } + } else { + if (i+1)%g.Cols == 0 { + col++ + row = 0 + } else { + row++ + } + } + i++ + } +} + +// MinSize finds the smallest size that satisfies all the child objects. +// For a GridLayout this is the size of the largest child object multiplied by +// the required number of columns and rows, with appropriate padding between +// children. +func (g *gridLayout) MinSize(objects []fyne.CanvasObject) fyne.Size { + rows := g.countRows(objects) + minSize := fyne.NewSize(0, 0) + for _, child := range objects { + if !child.Visible() { + continue + } + + minSize = minSize.Max(child.MinSize()) + } + + padding := theme.Padding() + + primaryObjects := rows + secondaryObjects := g.Cols + if g.horizontal() { + primaryObjects, secondaryObjects = secondaryObjects, primaryObjects + } + + width := minSize.Width * float32(primaryObjects) + height := minSize.Height * float32(secondaryObjects) + xpad := padding * fyne.Max(float32(primaryObjects-1), 0) + ypad := padding * fyne.Max(float32(secondaryObjects-1), 0) + + return fyne.NewSize(width+xpad, height+ypad) +} diff --git a/vendor/fyne.io/fyne/v2/layout/gridwraplayout.go b/vendor/fyne.io/fyne/v2/layout/gridwraplayout.go new file mode 100644 index 0000000..ec58aa5 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/layout/gridwraplayout.go @@ -0,0 +1,70 @@ +package layout + +import ( + "math" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/theme" +) + +// Declare conformity with Layout interface +var _ fyne.Layout = (*gridWrapLayout)(nil) + +type gridWrapLayout struct { + CellSize fyne.Size + colCount int + rowCount int +} + +// NewGridWrapLayout returns a new GridWrapLayout instance +func NewGridWrapLayout(size fyne.Size) fyne.Layout { + return &gridWrapLayout{size, 1, 1} +} + +// Layout is called to pack all child objects into a specified size. +// For a GridWrapLayout this will attempt to lay all the child objects in a row +// and wrap to a new row if the size is not large enough. +func (g *gridWrapLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) { + padding := theme.Padding() + g.colCount = 1 + g.rowCount = 0 + + if size.Width > g.CellSize.Width { + g.colCount = int(math.Floor(float64(size.Width+padding) / float64(g.CellSize.Width+padding))) + } + + i, x, y := 0, float32(0), float32(0) + for _, child := range objects { + if !child.Visible() { + continue + } + + if i%g.colCount == 0 { + g.rowCount++ + } + + child.Move(fyne.NewPos(x, y)) + child.Resize(g.CellSize) + + if (i+1)%g.colCount == 0 { + x = 0 + y += g.CellSize.Height + padding + } else { + x += g.CellSize.Width + padding + } + i++ + } +} + +// MinSize finds the smallest size that satisfies all the child objects. +// For a GridWrapLayout this is simply the specified cellsize as a single column +// layout has no padding. The returned size does not take into account the number +// of columns as this layout re-flows dynamically. +func (g *gridWrapLayout) MinSize(objects []fyne.CanvasObject) fyne.Size { + rows := g.rowCount + if rows < 1 { + rows = 1 + } + return fyne.NewSize(g.CellSize.Width, + (g.CellSize.Height*float32(rows))+(float32(rows-1)*theme.Padding())) +} diff --git a/vendor/fyne.io/fyne/v2/layout/paddedlayout.go b/vendor/fyne.io/fyne/v2/layout/paddedlayout.go new file mode 100644 index 0000000..d8ceb55 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/layout/paddedlayout.go @@ -0,0 +1,44 @@ +package layout + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/theme" +) + +// Declare conformity with Layout interface +var _ fyne.Layout = (*paddedLayout)(nil) + +type paddedLayout struct{} + +// Layout is called to pack all child objects into a specified size. +// For PaddedLayout this sets all children to the full size passed minus padding all around. +func (l paddedLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) { + padding := theme.Padding() + pos := fyne.NewSquareOffsetPos(padding) + siz := fyne.NewSize(size.Width-2*padding, size.Height-2*padding) + for _, child := range objects { + child.Resize(siz) + child.Move(pos) + } +} + +// MinSize finds the smallest size that satisfies all the child objects. +// For PaddedLayout this is determined simply as the MinSize of the largest child plus padding all around. +func (l paddedLayout) MinSize(objects []fyne.CanvasObject) (min fyne.Size) { + for _, child := range objects { + if !child.Visible() { + continue + } + + min = min.Max(child.MinSize()) + } + min = min.Add(fyne.NewSquareSize(2 * theme.Padding())) + return min +} + +// NewPaddedLayout creates a new PaddedLayout instance +// +// Since: 1.4 +func NewPaddedLayout() fyne.Layout { + return paddedLayout{} +} diff --git a/vendor/fyne.io/fyne/v2/layout/rowwrap.go b/vendor/fyne.io/fyne/v2/layout/rowwrap.go new file mode 100644 index 0000000..b66c3bd --- /dev/null +++ b/vendor/fyne.io/fyne/v2/layout/rowwrap.go @@ -0,0 +1,106 @@ +package layout + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/theme" +) + +type rowWrapLayout struct { + horizontalPadding float32 + minSize fyne.Size + verticalPadding float32 +} + +// NewRowWrapLayout returns a layout that dynamically arranges objects of similar height +// in rows and wraps them dynamically. +// Objects are separated with horizontal and vertical padding. +// +// Since: 2.7 +func NewRowWrapLayout() fyne.Layout { + p := theme.Padding() + return &rowWrapLayout{ + horizontalPadding: p, + verticalPadding: p, + } +} + +// NewRowWrapLayoutWithCustomPadding returns a new RowWrapLayout instance +// with custom horizontal and inner padding. +// +// Since: 2.7 +func NewRowWrapLayoutWithCustomPadding(horizontal, vertical float32) fyne.Layout { + return &rowWrapLayout{ + horizontalPadding: horizontal, + verticalPadding: vertical, + } +} + +var _ fyne.Layout = (*rowWrapLayout)(nil) + +// MinSize finds the smallest size that satisfies all the child objects. +// For a RowWrapLayout this is initially the width of the widest child +// and the height of the tallest child multiplied by the number of children, +// with appropriate padding between them. +// After Layout() has run it returns the actual min size. +func (l *rowWrapLayout) MinSize(objects []fyne.CanvasObject) fyne.Size { + if len(objects) == 0 { + return fyne.NewSize(0, 0) + } + if !l.minSize.IsZero() { + return l.minSize + } + var maxW, maxH float32 + var objCount int + for _, o := range objects { + if !o.Visible() { + continue + } + objCount++ + s := o.MinSize() + maxW = fyne.Max(maxW, s.Width) + maxH = fyne.Max(maxH, s.Height) + } + return fyne.NewSize(maxW, l.minHeight(maxH, objCount)) +} + +func (l *rowWrapLayout) minHeight(rowHeight float32, rowCount int) float32 { + return rowHeight*float32(rowCount) + l.verticalPadding*float32(rowCount-1) +} + +// Layout is called to pack all child objects into a specified size. +// For RowWrapLayout this will arrange all objects into rows of equal size +// and wrap objects into additional rows as needed. +func (l *rowWrapLayout) Layout(objects []fyne.CanvasObject, containerSize fyne.Size) { + if len(objects) == 0 { + return + } + var maxH float32 + for _, o := range objects { + if !o.Visible() { + continue + } + maxH = fyne.Max(maxH, o.MinSize().Height) + } + var minSize fyne.Size + pos := fyne.NewPos(0, 0) + rows := 1 + isFirst := true + for _, o := range objects { + if !o.Visible() { + continue + } + size := o.MinSize() + o.Resize(size) + if !isFirst && pos.X+size.Width+l.horizontalPadding >= containerSize.Width { + y := float32(rows) * (maxH + l.verticalPadding) + pos = fyne.NewPos(0, y) + rows++ + } + isFirst = false + minSize.Width = fyne.Max(minSize.Width, pos.X+size.Width) + minSize.Height = l.minHeight(maxH, rows) + o.Move(pos) + pos = pos.Add(fyne.NewPos(size.Width+l.horizontalPadding, 0)) + } + l.minSize = minSize +} diff --git a/vendor/fyne.io/fyne/v2/layout/spacer.go b/vendor/fyne.io/fyne/v2/layout/spacer.go new file mode 100644 index 0000000..589ea69 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/layout/spacer.go @@ -0,0 +1,80 @@ +package layout + +import "fyne.io/fyne/v2" + +// SpacerObject is any object that can be used to space out child objects +type SpacerObject interface { + ExpandVertical() bool + ExpandHorizontal() bool +} + +// Spacer is any simple object that can be used in a box layout to space +// out child objects +type Spacer struct { + FixHorizontal bool + FixVertical bool + + size fyne.Size + pos fyne.Position + hidden bool +} + +// NewSpacer returns a spacer object which can fill vertical and horizontal +// space. This is primarily used with a box layout. +func NewSpacer() fyne.CanvasObject { + return &Spacer{} +} + +// ExpandVertical returns whether or not this spacer expands on the vertical axis +func (s *Spacer) ExpandVertical() bool { + return !s.FixVertical +} + +// ExpandHorizontal returns whether or not this spacer expands on the horizontal axis +func (s *Spacer) ExpandHorizontal() bool { + return !s.FixHorizontal +} + +// Size returns the current size of this Spacer +func (s *Spacer) Size() fyne.Size { + return s.size +} + +// Resize sets a new size for the Spacer - this will be called by the layout +func (s *Spacer) Resize(size fyne.Size) { + s.size = size +} + +// Position returns the current position of this Spacer +func (s *Spacer) Position() fyne.Position { + return s.pos +} + +// Move sets a new position for the Spacer - this will be called by the layout +func (s *Spacer) Move(pos fyne.Position) { + s.pos = pos +} + +// MinSize returns a 0 size as a Spacer can shrink to no actual size +func (s *Spacer) MinSize() fyne.Size { + return fyne.NewSize(0, 0) +} + +// Visible returns true if this spacer should affect the layout +func (s *Spacer) Visible() bool { + return !s.hidden +} + +// Show sets the Spacer to be part of the layout calculations +func (s *Spacer) Show() { + s.hidden = false +} + +// Hide removes this Spacer from layout calculations +func (s *Spacer) Hide() { + s.hidden = true +} + +// Refresh does nothing for a spacer but is part of the CanvasObject definition +func (s *Spacer) Refresh() { +} diff --git a/vendor/fyne.io/fyne/v2/layout/stacklayout.go b/vendor/fyne.io/fyne/v2/layout/stacklayout.go new file mode 100644 index 0000000..a8f896a --- /dev/null +++ b/vendor/fyne.io/fyne/v2/layout/stacklayout.go @@ -0,0 +1,51 @@ +// Package layout defines the various layouts available to Fyne apps. +package layout // import "fyne.io/fyne/v2/layout" + +import "fyne.io/fyne/v2" + +// Declare conformity with Layout interface +var _ fyne.Layout = (*stackLayout)(nil) + +type stackLayout struct{} + +// NewStackLayout returns a new StackLayout instance. Objects are stacked +// on top of each other with later objects on top of those before. +// Having only a single object has no impact as CanvasObjects will +// fill the available space even without a Stack. +// +// Since: 2.4 +func NewStackLayout() fyne.Layout { + return stackLayout{} +} + +// NewMaxLayout creates a new MaxLayout instance +// +// Deprecated: Use layout.NewStackLayout() instead. +func NewMaxLayout() fyne.Layout { + return NewStackLayout() +} + +// Layout is called to pack all child objects into a specified size. +// For StackLayout this sets all children to the full size passed. +func (m stackLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) { + topLeft := fyne.NewPos(0, 0) + for _, child := range objects { + child.Resize(size) + child.Move(topLeft) + } +} + +// MinSize finds the smallest size that satisfies all the child objects. +// For StackLayout this is determined simply as the MinSize of the largest child. +func (m stackLayout) MinSize(objects []fyne.CanvasObject) fyne.Size { + minSize := fyne.NewSize(0, 0) + for _, child := range objects { + if !child.Visible() { + continue + } + + minSize = minSize.Max(child.MinSize()) + } + + return minSize +} diff --git a/vendor/fyne.io/fyne/v2/locale.go b/vendor/fyne.io/fyne/v2/locale.go new file mode 100644 index 0000000..a7b1bf4 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/locale.go @@ -0,0 +1,25 @@ +package fyne + +import "strings" + +// Locale represents a user's locale (language, region and script) +// +// Since: 2.5 +type Locale string + +// LanguageString returns a version of the local without the script portion. +// For example "en" or "fr-FR". +func (l Locale) LanguageString() string { + count := strings.Count(string(l), "-") + if count < 2 { + return string(l) + } + + pos := strings.LastIndex(string(l), "-") + return string(l)[:pos] +} + +// String returns the complete locale as a standard string. +func (l Locale) String() string { + return string(l) +} diff --git a/vendor/fyne.io/fyne/v2/log.go b/vendor/fyne.io/fyne/v2/log.go new file mode 100644 index 0000000..53cd1b2 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/log.go @@ -0,0 +1,21 @@ +package fyne + +import ( + "log" + "runtime" +) + +// LogError reports an error to the command line with the specified err cause, +// if not nil. +// The function also reports basic information about the code location. +func LogError(reason string, err error) { + log.Println("Fyne error: ", reason) + if err != nil { + log.Println(" Cause:", err) + } + + _, file, line, ok := runtime.Caller(1) + if ok { + log.Printf(" At: %s:%d", file, line) + } +} diff --git a/vendor/fyne.io/fyne/v2/math.go b/vendor/fyne.io/fyne/v2/math.go new file mode 100644 index 0000000..3f66064 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/math.go @@ -0,0 +1,17 @@ +package fyne + +// Min returns the smaller of the passed values. +func Min(x, y float32) float32 { + if x < y { + return x + } + return y +} + +// Max returns the larger of the passed values. +func Max(x, y float32) float32 { + if x > y { + return x + } + return y +} diff --git a/vendor/fyne.io/fyne/v2/menu.go b/vendor/fyne.io/fyne/v2/menu.go new file mode 100644 index 0000000..6cbbccf --- /dev/null +++ b/vendor/fyne.io/fyne/v2/menu.go @@ -0,0 +1,100 @@ +package fyne + +type systemTrayDriver interface { + Driver + SetSystemTrayMenu(*Menu) + SystemTrayMenu() *Menu +} + +// Menu stores the information required for a standard menu. +// A menu can pop down from a [MainMenu] or could be a pop out menu. +type Menu struct { + Label string + Items []*MenuItem +} + +// NewMenu creates a new menu given the specified label (to show in a [MainMenu]) and list of items to display. +func NewMenu(label string, items ...*MenuItem) *Menu { + return &Menu{Label: label, Items: items} +} + +// Refresh will instruct this menu to update its display. +// +// Since: 2.2 +func (m *Menu) Refresh() { + for _, w := range CurrentApp().Driver().AllWindows() { + main := w.MainMenu() + if main != nil { + for _, menu := range main.Items { + if menu == m { + w.SetMainMenu(main) + break + } + } + } + } + + if d, ok := CurrentApp().Driver().(systemTrayDriver); ok { + if m == d.SystemTrayMenu() { + d.SetSystemTrayMenu(m) + } + } +} + +// MenuItem is a single item within any menu, it contains a display Label and Action function that is called when tapped. +type MenuItem struct { + ChildMenu *Menu + // Since: 2.1 + IsQuit bool + IsSeparator bool + Label string + Action func() `json:"-"` + // Since: 2.1 + Disabled bool + // Since: 2.1 + Checked bool + // Since: 2.2 + Icon Resource + // Since: 2.2 + Shortcut Shortcut +} + +// NewMenuItem creates a new menu item from the passed label and action parameters. +func NewMenuItem(label string, action func()) *MenuItem { + return &MenuItem{Label: label, Action: action} +} + +// NewMenuItemWithIcon creates a new menu item from the passed label, icon, and action parameters. +// +// Since: 2.7 +func NewMenuItemWithIcon(label string, icon Resource, action func()) *MenuItem { + return &MenuItem{Label: label, Icon: icon, Action: action} +} + +// NewMenuItemSeparator creates a menu item that is to be used as a separator. +func NewMenuItemSeparator() *MenuItem { + return &MenuItem{IsSeparator: true, Action: func() {}} +} + +// MainMenu defines the data required to show a menu bar (desktop) or other appropriate top level menu. +type MainMenu struct { + Items []*Menu +} + +// NewMainMenu creates a top level menu structure used by fyne.Window for displaying a menubar +// (or appropriate equivalent). +func NewMainMenu(items ...*Menu) *MainMenu { + return &MainMenu{Items: items} +} + +// Refresh will instruct any rendered menus using this struct to update their display. +// +// Since: 2.2 +func (m *MainMenu) Refresh() { + for _, w := range CurrentApp().Driver().AllWindows() { + menu := w.MainMenu() + if menu != nil && menu == m { + w.SetMainMenu(m) + } + } +} diff --git a/vendor/fyne.io/fyne/v2/notification.go b/vendor/fyne.io/fyne/v2/notification.go new file mode 100644 index 0000000..a068623 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/notification.go @@ -0,0 +1,11 @@ +package fyne + +// Notification represents a user notification that can be sent to the operating system. +type Notification struct { + Title, Content string +} + +// NewNotification creates a notification that can be passed to [App.SendNotification]. +func NewNotification(title, content string) *Notification { + return &Notification{Title: title, Content: content} +} diff --git a/vendor/fyne.io/fyne/v2/overlay_stack.go b/vendor/fyne.io/fyne/v2/overlay_stack.go new file mode 100644 index 0000000..eb7c9c7 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/overlay_stack.go @@ -0,0 +1,13 @@ +package fyne + +// OverlayStack is a stack of [CanvasObject]s intended to be used as overlays of a [Canvas]. +type OverlayStack interface { + // Add adds an overlay on the top of the overlay stack. + Add(overlay CanvasObject) + // List returns the overlays currently on the overlay stack. + List() []CanvasObject + // Remove removes the given object and all objects above it from the overlay stack. + Remove(overlay CanvasObject) + // Top returns the top-most object of the overlay stack. + Top() CanvasObject +} diff --git a/vendor/fyne.io/fyne/v2/preferences.go b/vendor/fyne.io/fyne/v2/preferences.go new file mode 100644 index 0000000..ead1293 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/preferences.go @@ -0,0 +1,95 @@ +package fyne + +// Preferences describes the ways that an app can save and load user preferences +type Preferences interface { + // Bool looks up a bool value for the key + Bool(key string) bool + // BoolWithFallback looks up a bool value and returns the given fallback if not found + BoolWithFallback(key string, fallback bool) bool + // SetBool saves a bool value for the given key + SetBool(key string, value bool) + + // BoolList looks up a list of bool values for the key + // + // Since: 2.4 + BoolList(key string) []bool + // BoolListWithFallback looks up a list of bool values and returns the given fallback if not found + // + // Since: 2.4 + BoolListWithFallback(key string, fallback []bool) []bool + // SetBoolList saves a list of bool values for the given key + // + // Since: 2.4 + SetBoolList(key string, value []bool) + + // Float looks up a float64 value for the key + Float(key string) float64 + // FloatWithFallback looks up a float64 value and returns the given fallback if not found + FloatWithFallback(key string, fallback float64) float64 + // SetFloat saves a float64 value for the given key + SetFloat(key string, value float64) + + // FloatList looks up a list of float64 values for the key + // + // Since: 2.4 + FloatList(key string) []float64 + // FloatListWithFallback looks up a list of float64 values and returns the given fallback if not found + // + // Since: 2.4 + FloatListWithFallback(key string, fallback []float64) []float64 + // SetFloatList saves a list of float64 values for the given key + // + // Since: 2.4 + SetFloatList(key string, value []float64) + + // Int looks up an integer value for the key + Int(key string) int + // IntWithFallback looks up an integer value and returns the given fallback if not found + IntWithFallback(key string, fallback int) int + // SetInt saves an integer value for the given key + SetInt(key string, value int) + + // IntList looks up a list of int values for the key + // + // Since: 2.4 + IntList(key string) []int + // IntListWithFallback looks up a list of int values and returns the given fallback if not found + // + // Since: 2.4 + IntListWithFallback(key string, fallback []int) []int + // SetIntList saves a list of string values for the given key + // + // Since: 2.4 + SetIntList(key string, value []int) + + // String looks up a string value for the key + String(key string) string + // StringWithFallback looks up a string value and returns the given fallback if not found + StringWithFallback(key, fallback string) string + // SetString saves a string value for the given key + SetString(key string, value string) + + // StringList looks up a list of string values for the key + // + // Since: 2.4 + StringList(key string) []string + // StringListWithFallback looks up a list of string values and returns the given fallback if not found + // + // Since: 2.4 + StringListWithFallback(key string, fallback []string) []string + // SetStringList saves a list of string values for the given key + // + // Since: 2.4 + SetStringList(key string, value []string) + + // RemoveValue removes a value for the given key (not currently supported on iOS) + RemoveValue(key string) + + // AddChangeListener allows code to be notified when some preferences change. This will fire on any update. + AddChangeListener(func()) + + // ChangeListeners returns a list of the known change listeners for this preference set. + // + // Since: 2.3 + ChangeListeners() []func() +} diff --git a/vendor/fyne.io/fyne/v2/resource.go b/vendor/fyne.io/fyne/v2/resource.go new file mode 100644 index 0000000..95e3237 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/resource.go @@ -0,0 +1,84 @@ +package fyne + +import ( + "io" + "net/http" + "os" + "path/filepath" +) + +// Resource represents a single binary resource, such as an image or font. +// A resource has an identifying name and byte array content. +// The serialised path of a resource can be obtained which may result in a +// blocking filesystem write operation. +type Resource interface { + Name() string + Content() []byte +} + +// ThemedResource is a version of a resource that can be updated to match a certain theme color. +// The [ThemeColorName] will be used to look up the color for the current theme and colorize the resource. +// +// Since: 2.5 +type ThemedResource interface { + Resource + ThemeColorName() ThemeColorName +} + +// StaticResource is a bundled resource compiled into the application. +// These resources are normally generated by the fyne_bundle command included in +// the Fyne toolkit. +type StaticResource struct { + StaticName string + StaticContent []byte +} + +// Name returns the unique name of this resource, usually matching the file it +// was generated from. +func (r *StaticResource) Name() string { + return r.StaticName +} + +// Content returns the bytes of the bundled resource, no compression is applied +// but any compression on the resource is retained. +func (r *StaticResource) Content() []byte { + return r.StaticContent +} + +// NewStaticResource returns a new static resource object with the specified +// name and content. Creating a new static resource in memory results in +// sharable binary data that may be serialised to the system cache location. +func NewStaticResource(name string, content []byte) *StaticResource { + return &StaticResource{ + StaticName: name, + StaticContent: content, + } +} + +// LoadResourceFromPath creates a new [StaticResource] in memory using the contents of the specified file. +func LoadResourceFromPath(path string) (Resource, error) { + bytes, err := os.ReadFile(filepath.Clean(path)) + if err != nil { + return nil, err + } + + name := filepath.Base(path) + return NewStaticResource(name, bytes), nil +} + +// LoadResourceFromURLString creates a new [StaticResource] in memory using the body of the specified URL. +func LoadResourceFromURLString(urlStr string) (Resource, error) { + res, err := http.Get(urlStr) + if err != nil { + return nil, err + } + defer res.Body.Close() + + bytes, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + + name := filepath.Base(urlStr) + return NewStaticResource(name, bytes), nil +} diff --git a/vendor/fyne.io/fyne/v2/scroll.go b/vendor/fyne.io/fyne/v2/scroll.go new file mode 100644 index 0000000..98082d2 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/scroll.go @@ -0,0 +1,18 @@ +package fyne + +// ScrollDirection represents the directions in which a scrollable container or widget can scroll its child content. +// +// Since: 2.6 +type ScrollDirection int + +// Constants for valid values of ScrollDirection used in containers and widgets. +const ( + // ScrollBoth supports horizontal and vertical scrolling. + ScrollBoth ScrollDirection = iota + // ScrollHorizontalOnly specifies the scrolling should only happen left to right. + ScrollHorizontalOnly + // ScrollVerticalOnly specifies the scrolling should only happen top to bottom. + ScrollVerticalOnly + // ScrollNone turns off scrolling for this container. + ScrollNone +) diff --git a/vendor/fyne.io/fyne/v2/serialise.go b/vendor/fyne.io/fyne/v2/serialise.go new file mode 100644 index 0000000..e845fa3 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/serialise.go @@ -0,0 +1,26 @@ +package fyne + +import ( + "fmt" + "strings" +) + +// GoString converts a Resource object to Go code. +// This is useful if serialising to a Go file for compilation into a binary. +func (r *StaticResource) GoString() string { + buffer := strings.Builder{} + + buffer.WriteString("&fyne.StaticResource{\n\tStaticName: \"") + buffer.WriteString(r.StaticName) + buffer.WriteString("\",\n\tStaticContent: []byte{\n\t\t") + for i, v := range r.StaticContent { + if i > 0 { + buffer.WriteString(", ") + } + + fmt.Fprint(&buffer, v) + } + buffer.WriteString("}}") + + return buffer.String() +} diff --git a/vendor/fyne.io/fyne/v2/settings.go b/vendor/fyne.io/fyne/v2/settings.go new file mode 100644 index 0000000..234c4a6 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/settings.go @@ -0,0 +1,44 @@ +package fyne + +// BuildType defines different modes that an application can be built using. +type BuildType int + +const ( + // BuildStandard is the normal build mode - it is not debug, test or release mode. + BuildStandard BuildType = iota + // BuildDebug is used when a developer would like more information and visual output for app debugging. + BuildDebug + // BuildRelease is a final production build, it is like [BuildStandard] but will use distribution certificates. + // A release build is typically going to connect to live services and is not usually used during development. + BuildRelease +) + +// Settings describes the application configuration available. +type Settings interface { + Theme() Theme + SetTheme(Theme) + // ThemeVariant defines which preferred version of a theme should be used (i.e. light or dark) + // + // Since: 2.0 + ThemeVariant() ThemeVariant + Scale() float32 + // PrimaryColor indicates a user preference for a named primary color + // + // Since: 1.4 + PrimaryColor() string + + // AddChangeListener subscribes to settings change events over a channel. + // + // Deprecated: Use AddListener instead, which uses a callback-based API + // with the callback guaranteed to be invoked on the app goroutine. + AddChangeListener(chan Settings) + + // AddListener registers a callback that is invoked whenever the settings change. + // + // Since: 2.6 + AddListener(func(Settings)) + + BuildType() BuildType + + ShowAnimations() bool +} diff --git a/vendor/fyne.io/fyne/v2/shortcut.go b/vendor/fyne.io/fyne/v2/shortcut.go new file mode 100644 index 0000000..55c38b1 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/shortcut.go @@ -0,0 +1,172 @@ +package fyne + +import "sync" + +// ShortcutHandler is a default implementation of the shortcut handler +// for [CanvasObject]. +type ShortcutHandler struct { + entry sync.Map // map[string]func(Shortcut) +} + +// TypedShortcut handle the registered shortcut +func (sh *ShortcutHandler) TypedShortcut(shortcut Shortcut) { + val, ok := sh.entry.Load(shortcut.ShortcutName()) + if !ok { + return + } + + f := val.(func(Shortcut)) + f(shortcut) +} + +// AddShortcut register a handler to be executed when the shortcut action is triggered +func (sh *ShortcutHandler) AddShortcut(shortcut Shortcut, handler func(shortcut Shortcut)) { + sh.entry.Store(shortcut.ShortcutName(), handler) +} + +// RemoveShortcut removes a registered shortcut +func (sh *ShortcutHandler) RemoveShortcut(shortcut Shortcut) { + sh.entry.Delete(shortcut.ShortcutName()) +} + +// Shortcut is the interface used to describe a shortcut action +type Shortcut interface { + ShortcutName() string +} + +// KeyboardShortcut describes a shortcut meant to be triggered by a keyboard action. +type KeyboardShortcut interface { + Shortcut + Key() KeyName + Mod() KeyModifier +} + +var _ KeyboardShortcut = (*ShortcutPaste)(nil) + +// ShortcutPaste describes a shortcut paste action. +type ShortcutPaste struct { + Clipboard Clipboard +} + +// Key returns the [KeyName] for this shortcut. +func (se *ShortcutPaste) Key() KeyName { + return KeyV +} + +// Mod returns the [KeyModifier] for this shortcut. +func (se *ShortcutPaste) Mod() KeyModifier { + return KeyModifierShortcutDefault +} + +// ShortcutName returns the shortcut name +func (se *ShortcutPaste) ShortcutName() string { + return "Paste" +} + +var _ KeyboardShortcut = (*ShortcutCopy)(nil) + +// ShortcutCopy describes a shortcut copy action. +type ShortcutCopy struct { + Clipboard Clipboard +} + +// Key returns the [KeyName] for this shortcut. +func (se *ShortcutCopy) Key() KeyName { + return KeyC +} + +// Mod returns the [KeyModifier] for this shortcut. +func (se *ShortcutCopy) Mod() KeyModifier { + return KeyModifierShortcutDefault +} + +// ShortcutName returns the shortcut name +func (se *ShortcutCopy) ShortcutName() string { + return "Copy" +} + +var _ KeyboardShortcut = (*ShortcutCut)(nil) + +// ShortcutCut describes a shortcut cut action. +type ShortcutCut struct { + Clipboard Clipboard +} + +// Key returns the [KeyName] for this shortcut. +func (se *ShortcutCut) Key() KeyName { + return KeyX +} + +// Mod returns the [KeyModifier] for this shortcut. +func (se *ShortcutCut) Mod() KeyModifier { + return KeyModifierShortcutDefault +} + +// ShortcutName returns the shortcut name +func (se *ShortcutCut) ShortcutName() string { + return "Cut" +} + +var _ KeyboardShortcut = (*ShortcutSelectAll)(nil) + +// ShortcutSelectAll describes a shortcut selectAll action. +type ShortcutSelectAll struct{} + +// Key returns the [KeyName] for this shortcut. +func (se *ShortcutSelectAll) Key() KeyName { + return KeyA +} + +// Mod returns the [KeyModifier] for this shortcut. +func (se *ShortcutSelectAll) Mod() KeyModifier { + return KeyModifierShortcutDefault +} + +// ShortcutName returns the shortcut name +func (se *ShortcutSelectAll) ShortcutName() string { + return "SelectAll" +} + +var _ KeyboardShortcut = (*ShortcutUndo)(nil) + +// ShortcutUndo describes a shortcut undo action. +// +// Since: 2.5 +type ShortcutUndo struct{} + +// Key returns the [KeyName] for this shortcut. +func (se *ShortcutUndo) Key() KeyName { + return KeyZ +} + +// Mod returns the [KeyModifier] for this shortcut. +func (se *ShortcutUndo) Mod() KeyModifier { + return KeyModifierShortcutDefault +} + +// ShortcutName returns the shortcut name +func (se *ShortcutUndo) ShortcutName() string { + return "Undo" +} + +var _ KeyboardShortcut = (*ShortcutRedo)(nil) + +// ShortcutRedo describes a shortcut redo action. +// +// Since: 2.5 +type ShortcutRedo struct{} + +// Key returns the [KeyName] for this shortcut. +func (se *ShortcutRedo) Key() KeyName { + return KeyY +} + +// Mod returns the [KeyModifier] for this shortcut. +func (se *ShortcutRedo) Mod() KeyModifier { + return KeyModifierShortcutDefault +} + +// ShortcutName returns the shortcut name +func (se *ShortcutRedo) ShortcutName() string { + return "Redo" +} diff --git a/vendor/fyne.io/fyne/v2/staticcheck.conf b/vendor/fyne.io/fyne/v2/staticcheck.conf new file mode 100644 index 0000000..2817485 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/staticcheck.conf @@ -0,0 +1 @@ +checks = ["inherit", "ST1003", "ST1016", "ST1020", "ST1021", "ST1022"] diff --git a/vendor/fyne.io/fyne/v2/storage.go b/vendor/fyne.io/fyne/v2/storage.go new file mode 100644 index 0000000..f6f8585 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/storage.go @@ -0,0 +1,14 @@ +package fyne + +// Storage is used to manage file storage inside an application sandbox. +// The files managed by this interface are unique to the current application. +type Storage interface { + RootURI() URI + + Create(name string) (URIWriteCloser, error) + Open(name string) (URIReadCloser, error) + Save(name string) (URIWriteCloser, error) + Remove(name string) error + + List() []string +} diff --git a/vendor/fyne.io/fyne/v2/storage/errors.go b/vendor/fyne.io/fyne/v2/storage/errors.go new file mode 100644 index 0000000..34da267 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/storage/errors.go @@ -0,0 +1,15 @@ +package storage + +import "errors" + +var ( + // ErrAlreadyExists may be thrown by docs. E.g., save a document twice. + // + // Since: 2.3 + ErrAlreadyExists = errors.New("document already exists") + + // ErrNotExists may be thrown by docs. E.g., save an unknown document. + // + // Since: 2.3 + ErrNotExists = errors.New("document does not exist") +) diff --git a/vendor/fyne.io/fyne/v2/storage/file.go b/vendor/fyne.io/fyne/v2/storage/file.go new file mode 100644 index 0000000..42813fd --- /dev/null +++ b/vendor/fyne.io/fyne/v2/storage/file.go @@ -0,0 +1,48 @@ +// Package storage provides storage access and management functionality. +package storage + +import ( + "errors" + + "fyne.io/fyne/v2" +) + +// OpenFileFromURI loads a file read stream from a resource identifier. +// This is mostly provided so that file references can be saved using their URI and loaded again later. +// +// Deprecated: this has been replaced by storage.Reader(URI) +func OpenFileFromURI(uri fyne.URI) (fyne.URIReadCloser, error) { + return Reader(uri) +} + +// SaveFileToURI loads a file write stream to a resource identifier. +// This is mostly provided so that file references can be saved using their URI and written to again later. +// +// Deprecated: this has been replaced by storage.Writer(URI) +func SaveFileToURI(uri fyne.URI) (fyne.URIWriteCloser, error) { + return Writer(uri) +} + +// ListerForURI will attempt to use the application's driver to convert a +// standard URI into a listable URI. +// +// Since: 1.4 +func ListerForURI(uri fyne.URI) (fyne.ListableURI, error) { + listable, err := CanList(uri) + if err != nil { + return nil, err + } + if !listable { + return nil, errors.New("uri is not listable") + } + + return &legacyListable{uri}, nil +} + +type legacyListable struct { + fyne.URI +} + +func (l *legacyListable) List() ([]fyne.URI, error) { + return List(l.URI) +} diff --git a/vendor/fyne.io/fyne/v2/storage/filter.go b/vendor/fyne.io/fyne/v2/storage/filter.go new file mode 100644 index 0000000..07bfd12 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/storage/filter.go @@ -0,0 +1,65 @@ +package storage + +import ( + "strings" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/repository/mime" +) + +// FileFilter is an interface that can be implemented to provide a filter to a file dialog. +type FileFilter interface { + Matches(fyne.URI) bool +} + +// ExtensionFileFilter represents a file filter based on the ending of file names, +// for example ".txt" and ".png". +type ExtensionFileFilter struct { + Extensions []string +} + +// MimeTypeFileFilter represents a file filter based on the files mime type, +// for example "image/*", "audio/mp3". +type MimeTypeFileFilter struct { + MimeTypes []string +} + +// Matches returns true if a file URI has one of the filtered extensions. +func (e *ExtensionFileFilter) Matches(uri fyne.URI) bool { + extension := uri.Extension() + for _, ext := range e.Extensions { + if strings.EqualFold(extension, ext) { + return true + } + } + return false +} + +// NewExtensionFileFilter takes a string slice of extensions with a leading . and creates a filter for the file dialog. +// Example: .jpg, .mp3, .txt, .sh +func NewExtensionFileFilter(extensions []string) FileFilter { + return &ExtensionFileFilter{Extensions: extensions} +} + +// Matches returns true if a file URI has one of the filtered mimetypes. +func (mt *MimeTypeFileFilter) Matches(uri fyne.URI) bool { + mimeType, mimeSubType := mime.Split(uri.MimeType()) + for _, mimeTypeFull := range mt.MimeTypes { + mType, mSubType := mime.Split(mimeTypeFull) + if mType == "" || mSubType == "" { + continue + } + + mSubType, _, _ = strings.Cut(mSubType, ";") + if mType == mimeType && (mSubType == mimeSubType || mSubType == "*") { + return true + } + } + return false +} + +// NewMimeTypeFileFilter takes a string slice of mimetypes, including globs, and creates a filter for the file dialog. +// Example: image/*, audio/mp3, text/plain, application/* +func NewMimeTypeFileFilter(mimeTypes []string) FileFilter { + return &MimeTypeFileFilter{MimeTypes: mimeTypes} +} diff --git a/vendor/fyne.io/fyne/v2/storage/repository/errors.go b/vendor/fyne.io/fyne/v2/storage/repository/errors.go new file mode 100644 index 0000000..734d1dc --- /dev/null +++ b/vendor/fyne.io/fyne/v2/storage/repository/errors.go @@ -0,0 +1,24 @@ +package repository + +import ( + "errors" +) + +var ( + // ErrOperationNotSupported may be thrown by certain functions in the storage + // or repository packages which operate on URIs if an operation is attempted + // that is not supported for the scheme relevant to the URI, normally because + // the underlying repository has either not implemented the relevant function, + // or has explicitly returned this error. + // + // Since: 2.0 + ErrOperationNotSupported = errors.New("operation not supported for this URI") + + // ErrURIRoot should be thrown by fyne.URI implementations when the caller + // attempts to take the parent of the root. This way, downstream code that + // wants to programmatically walk up a URIs parent's will know when to stop + // iterating. + // + // Since: 2.0 + ErrURIRoot = errors.New("cannot take the parent of the root element in a URI") +) diff --git a/vendor/fyne.io/fyne/v2/storage/repository/generic.go b/vendor/fyne.io/fyne/v2/storage/repository/generic.go new file mode 100644 index 0000000..414c9fa --- /dev/null +++ b/vendor/fyne.io/fyne/v2/storage/repository/generic.go @@ -0,0 +1,322 @@ +package repository + +import ( + "io" + "path" + "strings" + + "fyne.io/fyne/v2" +) + +// GenericParent can be used as a common-case implementation of +// HierarchicalRepository.Parent(). It will create a parent URI based on +// IETF RFC3986. +// +// In short, the URI is separated into its component parts, the path component +// is split along instances of '/', and the trailing element is removed. The +// result is concatenated and parsed as a new URI. +// +// If the URI path is empty or '/', then a nil URI is returned, along with +// ErrURIRoot. +// +// NOTE: this function should not be called except by an implementation of +// the Repository interface - using this for unknown URIs may break. +// +// Since: 2.0 +func GenericParent(u fyne.URI) (fyne.URI, error) { + p := strings.TrimSuffix(u.Path(), "/") + if p == "" { + return nil, ErrURIRoot + } + + newURI := uri{ + scheme: u.Scheme(), + authority: u.Authority(), + path: path.Dir(p), + query: u.Query(), + fragment: u.Fragment(), + } + + // NOTE: we specifically want to use ParseURI, rather than &uri{}, + // since the repository for the URI we just created might be a + // CustomURIRepository that implements its own ParseURI. + // However, we can reuse &uri.String() to not duplicate string creation. + return ParseURI(newURI.String()) +} + +// GenericChild can be used as a common-case implementation of +// HierarchicalRepository.Child(). It will create a child URI by separating the +// URI into its component parts as described in IETF RFC 3986, then appending +// "/" + component to the path, then concatenating the result and parsing it as +// a new URI. +// +// NOTE: this function should not be called except by an implementation of +// the Repository interface - using this for unknown URIs may break. +// +// Since: 2.0 +func GenericChild(u fyne.URI, component string) (fyne.URI, error) { + newURI := uri{ + scheme: u.Scheme(), + authority: u.Authority(), + path: path.Join(u.Path(), component), + query: u.Query(), + fragment: u.Fragment(), + } + + // NOTE: we specifically want to use ParseURI, rather than &uri{}, + // since the repository for the URI we just created might be a + // CustomURIRepository that implements its own ParseURI. + // However, we can reuse &uri.String() to not duplicate string creation. + return ParseURI(newURI.String()) +} + +// GenericCopy can be used a common-case implementation of +// CopyableRepository.Copy(). It will perform the copy by obtaining a reader +// for the source URI, a writer for the destination URI, then writing the +// contents of the source to the destination. +// +// For obvious reasons, the destination URI must have a registered +// WritableRepository. +// +// NOTE: this function should not be called except by an implementation of +// the Repository interface - using this for unknown URIs may break. +// +// Since: 2.0 +func GenericCopy(source fyne.URI, destination fyne.URI) error { + // Look up repositories for the source and destination. + srcrepo, err := ForURI(source) + if err != nil { + return err + } + + dstrepo, err := ForURI(destination) + if err != nil { + return err + } + + // The destination must be writable. + destwrepo, ok := dstrepo.(WritableRepository) + if !ok { + return ErrOperationNotSupported + } + + if listable, ok := srcrepo.(ListableRepository); ok { + isParent, err := listable.CanList(source) + if err == nil && isParent { + if srcrepo != destwrepo { // cannot copy folders between repositories + return ErrOperationNotSupported + } + + return genericCopyMoveListable(source, destination, srcrepo, false) + } + } + + // Create a reader and a writer. + srcReader, err := srcrepo.Reader(source) + if err != nil { + return err + } + defer srcReader.Close() + + dstWriter, err := destwrepo.Writer(destination) + if err != nil { + return err + } + defer dstWriter.Close() + + // Perform the copy. + _, err = io.Copy(dstWriter, srcReader) + return err +} + +// GenericDeleteAll can be used a common-case implementation of +// DeletableRepository.DeleteAll(). It will perform the deletion by obtaining +// a list of all items in the URI, then deleting each one. +// +// For obvious reasons, the URI must be writable. +// +// NOTE: this function should not be called except by an implementation of +// the Repository interface - using this for unknown URIs may break. +// +// Since: 2.7 +func GenericDeleteAll(u fyne.URI) error { + repo, err := ForURI(u) + if err != nil { + return err + } + + wrepo, ok := repo.(WritableRepository) + if !ok { + return ErrOperationNotSupported + } + + lrepo, ok := repo.(ListableRepository) + if !ok { + return wrepo.Delete(u) + } + + return genericDeleteAll(u, wrepo, lrepo) +} + +// GenericMove can be used a common-case implementation of +// MovableRepository.Move(). It will perform the move by obtaining a reader +// for the source URI, a writer for the destination URI, then writing the +// contents of the source to the destination. Following this, the source +// will be deleted using WritableRepository.Delete. +// +// For obvious reasons, the source and destination URIs must both be writable. +// +// NOTE: this function should not be called except by an implementation of +// the Repository interface - using this for unknown URIs may break. +// +// Since: 2.0 +func GenericMove(source fyne.URI, destination fyne.URI) error { + // This looks a lot like GenericCopy(), but I duplicated the code + // to avoid having to look up the repositories more than once. + + // Look up repositories for the source and destination. + srcrepo, err := ForURI(source) + if err != nil { + return err + } + + dstrepo, err := ForURI(destination) + if err != nil { + return err + } + + // The source and destination must both be writable, since the source + // is being deleted, which requires WritableRepository. + destwrepo, ok := dstrepo.(WritableRepository) + if !ok { + return ErrOperationNotSupported + } + + srcwrepo, ok := srcrepo.(WritableRepository) + if !ok { + return ErrOperationNotSupported + } + + if listable, ok := srcrepo.(ListableRepository); ok { + isParent, err := listable.CanList(source) + if err == nil && isParent { + if srcrepo != destwrepo { // cannot move between repositories + return ErrOperationNotSupported + } + + return genericCopyMoveListable(source, destination, srcrepo, true) + } + } + + // Create the reader and writer to perform the copy operation. + srcReader, err := srcrepo.Reader(source) + if err != nil { + return err + } + + dstWriter, err := destwrepo.Writer(destination) + if err != nil { + return err + } + defer dstWriter.Close() + + // Perform the copy. + _, err = io.Copy(dstWriter, srcReader) + if err != nil { + return err + } + + // Finally, delete the source only if the move finished without error. + srcReader.Close() + return srcwrepo.Delete(source) +} + +func genericCopyMoveListable(source, destination fyne.URI, repo Repository, deleteSource bool) error { + lister, ok1 := repo.(ListableRepository) + mover, ok2 := repo.(MovableRepository) + copier, ok3 := repo.(CopyableRepository) + + if !ok1 || (deleteSource && !ok2) || (!deleteSource && !ok3) { + return ErrOperationNotSupported // cannot move a lister in a non-listable/movable repo + } + + err := lister.CreateListable(destination) + if err != nil { + return err + } + + list, err := lister.List(source) + if err != nil { + return err + } + for _, child := range list { + newChild, _ := repo.(HierarchicalRepository).Child(destination, child.Name()) + if deleteSource { + err = mover.Move(child, newChild) + } else { + err = copier.Copy(child, newChild) + } + if err != nil { + return err + } + } + + if !deleteSource { + return nil + } + // we know the repo is writable as well from earlier checks + writer, _ := repo.(WritableRepository) + return writer.Delete(source) +} + +func genericDeleteAll(u fyne.URI, wrepo WritableRepository, lrepo ListableRepository) error { + listable, err := lrepo.CanList(u) + if err != nil { + return err + } else if !listable { + return wrepo.Delete(u) + } + + children, err := lrepo.List(u) + if err != nil { + return err + } else if len(children) == 0 { + return wrepo.Delete(u) + } + + var folders []fyne.URI + var files []fyne.URI + for i := 0; i < len(children); i++ { + listable, err = lrepo.CanList(children[i]) + if err != nil { + return err + } + + if listable { + grandChildren, err := lrepo.List(children[i]) + if err != nil { + return err + } + folders = append(folders, children[i]) + children = append(children, grandChildren...) + } else { + files = append(files, children[i]) + } + } + + for i := len(files) - 1; i >= 0; i-- { + err = wrepo.Delete(files[i]) + if err != nil { + return err + } + } + + for i := len(folders) - 1; i >= 0; i-- { + err = wrepo.Delete(folders[i]) + if err != nil { + return err + } + } + + return wrepo.Delete(u) +} diff --git a/vendor/fyne.io/fyne/v2/storage/repository/parse.go b/vendor/fyne.io/fyne/v2/storage/repository/parse.go new file mode 100644 index 0000000..6ac50da --- /dev/null +++ b/vendor/fyne.io/fyne/v2/storage/repository/parse.go @@ -0,0 +1,114 @@ +package repository + +import ( + "errors" + "path/filepath" + "runtime" + "strings" + + uriParser "github.com/fredbi/uri" + + "fyne.io/fyne/v2" +) + +// NewFileURI implements the back-end logic to storage.NewFileURI, which you +// should use instead. This is only here because other functions in repository +// need to call it, and it prevents a circular import. +// +// Since: 2.0 +func NewFileURI(path string) fyne.URI { + // URIs are supposed to use forward slashes. On Windows, it + // should be OK to use the platform native filepath with UNIX + // or NT style paths, with / or \, but when we reconstruct + // the URI, we want to have / only. + if runtime.GOOS == "windows" { + // seems that sometimes we end up with + // double-backslashes + path = filepath.ToSlash(path) + } + + return &uri{ + scheme: "file", + path: path, + } +} + +// ParseURI implements the back-end logic for storage.ParseURI, which you +// should use instead. This is only here because other functions in repository +// need to call it, and it prevents a circular import. +// +// Since: 2.0 +func ParseURI(s string) (fyne.URI, error) { + // Extract the scheme. + scheme, path, ok := strings.Cut(s, ":") + if !ok { + return nil, errors.New("invalid URI, scheme must be present") + } + + if strings.EqualFold(scheme, "file") { + // Does this really deserve to be special? In principle, the + // purpose of this check is to pass it to NewFileURI, which + // allows platform path seps in the URI (against the RFC, but + // easier for people building URIs naively on Windows). Maybe + // we should punt this to whoever generated the URI in the + // first place? + + if len(path) <= 2 { // I.e. file: and // given we know scheme. + return nil, errors.New("not a valid URI") + } + + if path[:2] == "//" { + path = path[2:] + } + + // Windows files can break authority checks, so just return the parsed file URI + return NewFileURI(path), nil + } + + scheme = strings.ToLower(scheme) + repo, err := ForScheme(scheme) + if err == nil { + // If the repository registered for this scheme implements a parser + if c, ok := repo.(CustomURIRepository); ok { + return c.ParseURI(s) + } + } + + // There was no repository registered, or it did not provide a parser + + l, err := uriParser.Parse(s) + if err != nil { + return nil, err + } + + authority := l.Authority() + authBuilder := strings.Builder{} + authBuilder.Grow(len(authority.UserInfo()) + len(authority.Host()) + len(authority.Port()) + len("@[]:")) + + if userInfo := authority.UserInfo(); userInfo != "" { + authBuilder.WriteString(userInfo) + authBuilder.WriteByte('@') + } + + // Per RFC 3986, section 3.2.2, IPv6 addresses must be enclosed in square brackets. + if host := authority.Host(); strings.Contains(host, ":") { + authBuilder.WriteByte('[') + authBuilder.WriteString(host) + authBuilder.WriteByte(']') + } else { + authBuilder.WriteString(host) + } + + if port := authority.Port(); port != "" { + authBuilder.WriteByte(':') + authBuilder.WriteString(port) + } + + return &uri{ + scheme: scheme, + authority: authBuilder.String(), + path: authority.Path(), + query: l.Query().Encode(), + fragment: l.Fragment(), + }, nil +} diff --git a/vendor/fyne.io/fyne/v2/storage/repository/repository.go b/vendor/fyne.io/fyne/v2/storage/repository/repository.go new file mode 100644 index 0000000..763e16e --- /dev/null +++ b/vendor/fyne.io/fyne/v2/storage/repository/repository.go @@ -0,0 +1,301 @@ +// Package repository provides primitives for working with storage repositories. +package repository + +import ( + "fmt" + "strings" + + "fyne.io/fyne/v2" +) + +// repositoryTable stores the mapping of schemes to Repository implementations. +// It should only ever be used by ForURI() and Register(). +var repositoryTable = map[string]Repository{} + +// Repository represents a storage repository, which is a set of methods which +// implement specific functions on a URI. Repositories are registered to handle +// specific URI schemes, and the higher-level functions that operate on URIs +// internally look up an appropriate method from the relevant Repository. +// +// The repository interface includes only methods which must be implemented at +// a minimum. Without implementing all of the methods in this interface, a URI +// would not be usable in a useful way. Additional functionality can be exposed +// by using interfaces which extend Repository. +// +// Repositories are registered to handle a specific URI scheme (or schemes) +// using the Register() method. When a higher-level URI function such as +// storage.Copy() is called, the storage package will internally look up +// the repository associated with the scheme of the URI, then it will use +// a type assertion to check if the repository implements CopyableRepository. +// If so, the Copy() function will be run from the repository, otherwise +// storage.Copy() will return NotSupportedError. This works similarly for +// all other methods in repository-related interfaces. +// +// Note that a repository can be registered for multiple URI schemes. In such +// cases, the repository must internally select and implement the correct +// behavior for each URI scheme. +// +// A repository will only ever need to handle URIs with schemes for which it +// was registered, with the exception that functions with more than 1 operand +// such as Copy() and Move(), in which cases only the first operand is +// guaranteed to match a scheme for which the repository is registered. +// +// NOTE: most developers who use Fyne should *not* generally attempt to +// call repository methods directly. You should use the methods in the storage +// package, which will automatically detect the scheme of a URI and call into +// the appropriate repository. +// +// Since: 2.0 +type Repository interface { + // Exists will be used to implement calls to storage.Exists() for the + // registered scheme of this repository. + // + // Since: 2.0 + Exists(u fyne.URI) (bool, error) + + // Reader will be used to implement calls to storage.Reader() + // for the registered scheme of this repository. + // + // Since: 2.0 + Reader(u fyne.URI) (fyne.URIReadCloser, error) + + // CanRead will be used to implement calls to storage.CanRead() for the + // registered scheme of this repository. + // + // Since: 2.0 + CanRead(u fyne.URI) (bool, error) + + // Destroy is called when the repository is un-registered from a given + // URI scheme. + // + // The string parameter will be the URI scheme that the repository was + // registered for. This may be useful for repositories that need to + // handle more than one URI scheme internally. + // + // Since: 2.0 + Destroy(string) +} + +// CustomURIRepository is an extension of the repository interface which +// allows the behavior of storage.ParseURI to be overridden. This is only +// needed if you wish to generate custom URI types, rather than using Fyne's +// URI implementation and net/url based parsing. +// +// NOTE: even for URIs with non-RFC3986-compliant encoding, the URI MUST begin +// with 'scheme:', or storage.ParseURI() will not be able to determine which +// storage repository to delegate to for parsing. +// +// Since: 2.0 +type CustomURIRepository interface { + Repository + + // ParseURI will be used to implement calls to storage.ParseURI() + // for the registered scheme of this repository. + ParseURI(string) (fyne.URI, error) +} + +// WritableRepository is an extension of the Repository interface which also +// supports obtaining a writer for URIs of the scheme it is registered to. +// +// Since: 2.0 +type WritableRepository interface { + Repository + + // Writer will be used to implement calls to storage.WriterTo() for + // the registered scheme of this repository. + // + // Since: 2.0 + Writer(u fyne.URI) (fyne.URIWriteCloser, error) + + // CanWrite will be used to implement calls to storage.CanWrite() for + // the registered scheme of this repository. + // + // Since: 2.0 + CanWrite(u fyne.URI) (bool, error) + + // Delete will be used to implement calls to storage.Delete() for the + // registered scheme of this repository. + // + // Since: 2.0 + Delete(u fyne.URI) error +} + +// AppendableRepository is an extension of the WritableRepository interface which also +// supports opening a writer for URIs in append mode, without truncating their contents +// +// Since: 2.6 +type AppendableRepository interface { + WritableRepository + + // Appender will be used to call a Writer without truncating the + // file if it exists + // + // Since: 2.6 + Appender(u fyne.URI) (fyne.URIWriteCloser, error) +} + +// ListableRepository is an extension of the Repository interface which also +// supports obtaining directory listings (generally analogous to a directory +// listing) for URIs of the scheme it is registered to. +// +// Since: 2.0 +type ListableRepository interface { + Repository + + // CanList will be used to implement calls to storage.Listable() for + // the registered scheme of this repository. + // + // Since: 2.0 + CanList(u fyne.URI) (bool, error) + + // List will be used to implement calls to storage.List() for the + // registered scheme of this repository. + // + // Since: 2.0 + List(u fyne.URI) ([]fyne.URI, error) + + // CreateListable will be used to implement calls to + // storage.CreateListable() for the registered scheme of this + // repository. + // + // Since: 2.0 + CreateListable(u fyne.URI) error +} + +// HierarchicalRepository is an extension of the Repository interface which +// also supports determining the parent and child items of a URI. +// +// Since: 2.0 +type HierarchicalRepository interface { + Repository + + // Parent will be used to implement calls to storage.Parent() for the + // registered scheme of this repository. + // + // A generic implementation is provided in GenericParent(), which + // is based on the RFC3986 definition of a URI parent. + // + // Since: 2.0 + Parent(fyne.URI) (fyne.URI, error) + + // Child will be used to implement calls to storage.Child() for + // the registered scheme of this repository. + // + // A generic implementation is provided in GenericParent(), which + // is based on RFC3986. + // + // Since: 2.0 + Child(fyne.URI, string) (fyne.URI, error) +} + +// DeleteAllRepository is an extension of the WritableRepository interface which +// also supports deleting a URI and all its children. +// +// Since: 2.7 +type DeleteAllRepository interface { + WritableRepository + + // DeleteAll will be used to implement calls to storage.DeleteAll() for the + // registered scheme of this repository. + // + // A generic implementation is provided by GenericDeleteAll(). + // + // Since: 2.7 + DeleteAll(fyne.URI) error +} + +// CopyableRepository is an extension of the Repository interface which also +// supports copying referenced resources from one URI to another. +// +// Since: 2.0 +type CopyableRepository interface { + Repository + + // Copy will be used to implement calls to storage.Copy() for the + // registered scheme of this repository. + // + // A generic implementation is provided by GenericCopy(). + // + // NOTE: the first parameter is the source, the second is the + // destination. + // + // NOTE: if storage.Copy() is given two URIs of different schemes, it + // is possible that only the source URI will be of the type this + // repository is registered to handle. In such cases, implementations + // are suggested to fail-over to GenericCopy(). + // + // Since: 2.0 + Copy(fyne.URI, fyne.URI) error +} + +// MovableRepository is an extension of the Repository interface which also +// supports moving referenced resources from one URI to another. +// +// Note: both Moveable and Movable are correct spellings, but Movable is newer +// and more accepted. Source: https://grammarist.com/spelling/movable-moveable/ +// +// Since: 2.0 +type MovableRepository interface { + Repository + + // Move will be used to implement calls to storage.Move() for the + // registered scheme of this repository. + // + // A generic implementation is provided by GenericMove(). + // + // NOTE: the first parameter is the source, the second is the + // destination. + // + // NOTE: if storage.Move() is given two URIs of different schemes, it + // is possible that only the source URI will be of the type this + // repository is registered to handle. In such cases, implementations + // are suggested to fail-over to GenericMove(). + // + // Since: 2.0 + Move(fyne.URI, fyne.URI) error +} + +// Register registers a storage repository so that operations on URIs of the +// registered scheme will use methods implemented by the relevant repository +// implementation. +// +// Since: 2.0 +func Register(scheme string, repository Repository) { + scheme = strings.ToLower(scheme) + + if prev, ok := repositoryTable[scheme]; ok { + prev.Destroy(scheme) + } + + repositoryTable[scheme] = repository +} + +// ForURI returns the Repository instance which is registered to handle URIs of +// the given scheme. This is a helper method that calls ForScheme() on the +// scheme of the given URI. +// +// NOTE: this function is intended to be used specifically by the storage +// package. It generally should not be used outside of the fyne package - +// instead you should use the methods in the storage package. +// +// Since: 2.0 +func ForURI(u fyne.URI) (Repository, error) { + return ForScheme(u.Scheme()) +} + +// ForScheme returns the Repository instance which is registered to handle URIs +// of the given scheme. +// +// NOTE: this function is intended to be used specifically by the storage +// package. It generally should not be used outside of the fyne package - +// instead you should use the methods in the storage package. +// +// Since: 2.0 +func ForScheme(scheme string) (Repository, error) { + repo, ok := repositoryTable[scheme] + if !ok { + return nil, fmt.Errorf("no repository registered for scheme '%s'", scheme) + } + + return repo, nil +} diff --git a/vendor/fyne.io/fyne/v2/storage/repository/uri.go b/vendor/fyne.io/fyne/v2/storage/repository/uri.go new file mode 100644 index 0000000..34348de --- /dev/null +++ b/vendor/fyne.io/fyne/v2/storage/repository/uri.go @@ -0,0 +1,114 @@ +package repository + +import ( + "bufio" + "mime" + "path" + "strings" + "unicode/utf8" + + "fyne.io/fyne/v2" +) + +// EqualURI returns true if the two URIs are equal. +// +// Since: 2.6 +func EqualURI(t1, t2 fyne.URI) bool { + if t1 == nil || t2 == nil { + return t1 == t2 + } + + u1, ok1 := t1.(*uri) + u2, ok2 := t2.(*uri) + if ok1 && ok2 { + // Knowing the type, pointers are either the same or fields are the same. + return u1 == u2 || *u1 == *u2 + } + + return t1 == t2 || t1.String() == t2.String() +} + +// Declare conformance with fyne.URI interface. +var _ fyne.URI = &uri{} + +type uri struct { + scheme string + authority string + path string + query string + fragment string +} + +func (u *uri) Extension() string { + return path.Ext(u.path) +} + +func (u *uri) Name() string { + return path.Base(u.path) +} + +func (u *uri) MimeType() string { + mimeTypeFull := mime.TypeByExtension(u.Extension()) + if mimeTypeFull == "" { + mimeTypeFull = "text/plain" + + repo, err := ForURI(u) + if err != nil { + return "application/octet-stream" + } + + readCloser, err := repo.Reader(u) + if err == nil { + defer readCloser.Close() + scanner := bufio.NewScanner(readCloser) + if scanner.Scan() && !utf8.Valid(scanner.Bytes()) { + mimeTypeFull = "application/octet-stream" + } + } + } + + mimeType, _, _ := strings.Cut(mimeTypeFull, ";") + return mimeType +} + +func (u *uri) Scheme() string { + return u.scheme +} + +func (u *uri) String() string { + // NOTE: this string reconstruction is mandated by IETF RFC3986, + // section 5.3, pp. 35. + s := strings.Builder{} + s.Grow(len(u.scheme) + len(u.authority) + len(u.path) + len(u.query) + len(u.fragment) + len("://?#")) + + s.WriteString(u.scheme) + s.WriteString("://") + s.WriteString(u.authority) + s.WriteString(u.path) + + if len(u.query) > 0 { + s.WriteByte('?') + s.WriteString(u.query) + } + if len(u.fragment) > 0 { + s.WriteByte('#') + s.WriteString(u.fragment) + } + return s.String() +} + +func (u *uri) Authority() string { + return u.authority +} + +func (u *uri) Path() string { + return u.path +} + +func (u *uri) Query() string { + return u.query +} + +func (u *uri) Fragment() string { + return u.fragment +} diff --git a/vendor/fyne.io/fyne/v2/storage/resource.go b/vendor/fyne.io/fyne/v2/storage/resource.go new file mode 100644 index 0000000..d246d85 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/storage/resource.go @@ -0,0 +1,25 @@ +package storage + +import ( + "io" + + "fyne.io/fyne/v2" +) + +// LoadResourceFromURI creates a new StaticResource in memory using the contents of the specified URI. +// The URI will be opened using the current driver, so valid schemas will vary from platform to platform. +// The file:// schema will always work. +func LoadResourceFromURI(u fyne.URI) (fyne.Resource, error) { + read, err := Reader(u) + if err != nil { + return nil, err + } + + defer read.Close() + bytes, err := io.ReadAll(read) + if err != nil { + return nil, err + } + + return fyne.NewStaticResource(u.Name(), bytes), nil +} diff --git a/vendor/fyne.io/fyne/v2/storage/uri.go b/vendor/fyne.io/fyne/v2/storage/uri.go new file mode 100644 index 0000000..eb7977d --- /dev/null +++ b/vendor/fyne.io/fyne/v2/storage/uri.go @@ -0,0 +1,611 @@ +package storage + +import ( + "path" + "path/filepath" + "runtime" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/storage/repository" +) + +// EqualURI returns true if the two URIs are equal. +// +// Since: 2.6 +func EqualURI(t1, t2 fyne.URI) bool { + return repository.EqualURI(t1, t2) +} + +// NewFileURI creates a new URI from the given file path. +// Relative paths will be converted to absolute using filepath.Abs if required. +func NewFileURI(fpath string) fyne.URI { + if !(path.IsAbs(fpath) || runtime.GOOS == "windows" && filepath.IsAbs(fpath)) { + absolute, err := filepath.Abs(fpath) + if err == nil { + fpath = absolute + } + } + + return repository.NewFileURI(fpath) +} + +// NewURI creates a new URI from the given string representation. This could be +// a URI from an external source or one saved from URI.String() +// +// Deprecated: use ParseURI instead +func NewURI(s string) fyne.URI { + u, _ := ParseURI(s) + return u +} + +// ParseURI creates a new URI instance by parsing a URI string. +// +// Parse URI will parse up to the first ':' present in the URI string to +// extract the scheme, and then delegate further parsing to the registered +// repository for the given scheme. If no repository is registered for that +// scheme, the URI is parsed on a best-effort basis using net/url. +// +// As a special exception, URIs beginning with 'file:' are always parsed using +// NewFileURI(), which will correctly handle back-slashes appearing in the URI +// path component on Windows. +// +// Since: 2.0 +func ParseURI(s string) (fyne.URI, error) { + return repository.ParseURI(s) +} + +// Parent returns a URI referencing the parent resource of the resource +// referenced by the URI. For example, the Parent() of 'file://foo/bar.baz' is +// 'file://foo'. The URI which is returned will be listable. +// +// NOTE: it is not a given that Parent() return a parent URI with the same +// Scheme(), though this will normally be the case. +// +// This can fail in several ways: +// +// - If the URI refers to a filesystem root, then the Parent() implementation +// must return (nil, URIRootError). +// +// - If the URI refers to a resource which does not exist in a hierarchical +// context (e.g. the URI references something which does not have a +// semantically meaningful "parent"), the Parent() implementation may return +// an error. +// +// - If determining the parent of the referenced resource requires +// interfacing with some external system, failures may propagate +// through the Parent() implementation. For example if determining +// the parent of a file:// URI requires reading information from +// the filesystem, it could fail with a permission error. +// +// - If the scheme of the given URI does not have a registered +// HierarchicalRepository instance, then this method will fail with a +// repository.ErrOperationNotSupported. +// +// NOTE: since v2.0.0, Parent() is backed by the repository system - this +// function is a helper which calls into an appropriate repository instance for +// the scheme of the URI it is given. +// +// Since: 1.4 +func Parent(u fyne.URI) (fyne.URI, error) { + repo, err := repository.ForURI(u) + if err != nil { + return nil, err + } + + hrepo, ok := repo.(repository.HierarchicalRepository) + if !ok { + return nil, repository.ErrOperationNotSupported + } + + return hrepo.Parent(u) +} + +// Child returns a URI referencing a resource nested hierarchically below the +// given URI, identified by a string. For example, the child with the string +// component 'quux' of 'file://foo/bar' is 'file://foo/bar/quux'. +// +// This can fail in several ways: +// +// - If the URI refers to a resource which does not exist in a hierarchical +// context (e.g. the URI references something which does not have a +// semantically meaningful "child"), the Child() implementation may return an +// error. +// +// - If generating a reference to a child of the referenced resource requires +// interfacing with some external system, failures may propagate through the +// Child() implementation. It is expected that this case would occur very +// rarely if ever. +// +// - If the scheme of the given URI does not have a registered +// HierarchicalRepository instance, then this method will fail with a +// repository.ErrOperationNotSupported. +// +// NOTE: since v2.0.0, Child() is backed by the repository system - this +// function is a helper which calls into an appropriate repository instance for +// the scheme of the URI it is given. +// +// Since: 1.4 +func Child(u fyne.URI, component string) (fyne.URI, error) { + repo, err := repository.ForURI(u) + if err != nil { + return nil, err + } + + hrepo, ok := repo.(repository.HierarchicalRepository) + if !ok { + return nil, repository.ErrOperationNotSupported + } + + return hrepo.Child(u, component) +} + +// Exists determines if the resource referenced by the URI exists. +// +// This can fail in several ways: +// +// - If checking the existence of a resource requires interfacing with some +// external system, then failures may propagate through Exists(). For +// example, checking the existence of a resource requires reading a directory +// may result in a permissions error. +// +// It is understood that a non-nil error value signals that the existence or +// non-existence of the resource cannot be determined and is undefined. +// +// NOTE: since v2.0.0, Exists is backed by the repository system - this function +// calls into a scheme-specific implementation from a registered repository. +// +// Exists may call into either a generic implementation, or into a +// scheme-specific implementation depending on which storage repositories have +// been registered. +// +// Since: 1.4 +func Exists(u fyne.URI) (bool, error) { + repo, err := repository.ForURI(u) + if err != nil { + return false, err + } + + return repo.Exists(u) +} + +// Delete destroys, deletes, or otherwise removes the resource referenced +// by the URI. +// +// This can fail in several ways: +// +// - If removing the resource requires interfacing with some external system, +// failures may propagate through Destroy(). For example, deleting a file may +// fail with a permissions error. +// +// - If the referenced resource does not exist, attempting to destroy it should +// throw an error. +// +// - If the scheme of the given URI does not have a registered +// WritableRepository instance, then this method will fail with a +// repository.ErrOperationNotSupported. +// +// Delete is backed by the repository system - this function calls +// into a scheme-specific implementation from a registered repository. +// +// Since: 2.0 +func Delete(u fyne.URI) error { + repo, err := repository.ForURI(u) + if err != nil { + return err + } + + wrepo, ok := repo.(repository.WritableRepository) + if !ok { + return repository.ErrOperationNotSupported + } + + return wrepo.Delete(u) +} + +// DeleteAll destroys, deletes, or otherwise removes the resource referenced +// by the URI and any child resources for listable URIs. +// +// DeleteAll is backed by the repository system - this function calls +// into a scheme-specific implementation from a registered repository. +// +// Since: 2.7 +func DeleteAll(u fyne.URI) error { + repo, err := repository.ForURI(u) + if err != nil { + return err + } + + drepo, ok := repo.(repository.DeleteAllRepository) + if !ok { + return repository.GenericDeleteAll(u) + } + + return drepo.DeleteAll(u) +} + +// Reader returns URIReadCloser set up to read from the resource that the +// URI references. +// +// This method can fail in several ways: +// +// - Different permissions or credentials are required to read the +// referenced resource. +// +// - This URI scheme could represent some resources that can be read, +// but this particular URI references a resources that is not +// something that can be read. +// +// - Attempting to set up the reader depended on a lower level +// operation such as a network or filesystem access that has failed +// in some way. +// +// Reader is backed by the repository system - this function calls +// into a scheme-specific implementation from a registered repository. +// +// Since: 2.0 +func Reader(u fyne.URI) (fyne.URIReadCloser, error) { + repo, err := repository.ForURI(u) + if err != nil { + return nil, err + } + + return repo.Reader(u) +} + +// CanRead determines if a given URI could be written to using the Reader() +// method. It is preferred to check if a URI is readable using this method +// before calling Reader(), because the underlying operations required to +// attempt to read and then report an error may be slower than the operations +// needed to test if a URI is readable. Keep in mind however that even if +// CanRead returns true, you must still do appropriate error handling for +// Reader(), as the underlying filesystem may have changed since you called +// CanRead. +// +// The non-existence of a resource should not be treated as an error. In other +// words, a Repository implementation which for some URI u returns false, nil +// for Exists(u), CanRead(u) should also return false, nil. +// +// CanRead is backed by the repository system - this function calls into a +// scheme-specific implementation from a registered repository. +// +// Since: 2.0 +func CanRead(u fyne.URI) (bool, error) { + repo, err := repository.ForURI(u) + if err != nil { + return false, err + } + + return repo.CanRead(u) +} + +// Writer returns URIWriteCloser set up to write to the resource that the +// URI references. +// +// Writing to a non-extant resource should create that resource if possible +// (and if not possible, this should be reflected in the return of CanWrite()). +// Writing to an extant resource should overwrite it in-place. At present, this +// API does not provide a mechanism for appending to an already-extant +// resource, except for reading it in and writing all the data back out. +// +// This method can fail in several ways: +// +// - Different permissions or credentials are required to write to the +// referenced resource. +// +// - This URI scheme could represent some resources that can be +// written, but this particular URI references a resources that is +// not something that can be written. +// +// - Attempting to set up the writer depended on a lower level +// operation such as a network or filesystem access that has failed +// in some way. +// +// - If the scheme of the given URI does not have a registered +// WritableRepository instance, then this method will fail with a +// repository.ErrOperationNotSupported. +// +// Writer is backed by the repository system - this function calls into a +// scheme-specific implementation from a registered repository. +// +// Since: 2.0 +func Writer(u fyne.URI) (fyne.URIWriteCloser, error) { + repo, err := repository.ForURI(u) + if err != nil { + return nil, err + } + + wrepo, ok := repo.(repository.WritableRepository) + if !ok { + return nil, repository.ErrOperationNotSupported + } + + return wrepo.Writer(u) +} + +// Appender returns URIWriteCloser set up to write to the resource that the +// URI references without truncating it first +// +// Writing to a non-extant resource should create that resource if possible +// (and if not possible, this should be reflected in the return of CanWrite()). +// Writing to an extant resource should NOT overwrite it in-place. +// +// This method can fail in several ways: +// +// - Different permissions or credentials are required to write to the +// referenced resource. +// +// - This URI scheme could represent some resources that can be +// written, but this particular URI references a resources that is +// not something that can be written. +// +// - Attempting to set up the writer depended on a lower level +// operation such as a network or filesystem access that has failed +// in some way. +// +// - If the scheme of the given URI does not have a registered +// AppendableRepository instance, then this method will fail with a +// repository.ErrOperationNotSupported. +// +// Appender is backed by the repository system - this function calls into a +// scheme-specific implementation from a registered repository. +// +// Since: 2.6 +func Appender(u fyne.URI) (fyne.URIWriteCloser, error) { + repo, err := repository.ForURI(u) + if err != nil { + return nil, err + } + + wrepo, ok := repo.(repository.AppendableRepository) + if !ok { + return nil, repository.ErrOperationNotSupported + } + + return wrepo.Appender(u) +} + +// CanWrite determines if a given URI could be written to using the Writer() +// method. It is preferred to check if a URI is writable using this method +// before calling Writer(), because the underlying operations required to +// attempt to write and then report an error may be slower than the operations +// needed to test if a URI is writable. Keep in mind however that even if +// CanWrite returns true, you must still do appropriate error handling for +// Writer(), as the underlying filesystem may have changed since you called +// CanWrite. + +// CanWrite is backed by the repository system - this function calls into a +// scheme-specific implementation from a registered repository. +// +// Since: 2.0 +func CanWrite(u fyne.URI) (bool, error) { + repo, err := repository.ForURI(u) + if err != nil { + return false, err + } + + wrepo, ok := repo.(repository.WritableRepository) + if !ok { + return false, repository.ErrOperationNotSupported + } + + return wrepo.CanWrite(u) +} + +// Copy given two URIs, 'src', and 'dest' both of the same scheme, will copy +// one to the other. If the source and destination are of different schemes, +// then the Copy implementation for the storage repository registered to the +// scheme of the source will be used. Implementations are recommended to use +// repository.GenericCopy() as a fail-over in the case that they do not +// understand how to operate on the scheme of the destination URI. However, the +// behavior of calling Copy() on URIs of non-matching schemes is ultimately +// defined by the storage repository registered to the scheme of the source +// URI. +// +// This method may fail in several ways: +// +// - Different permissions or credentials are required to perform the +// copy operation. +// +// - This URI scheme could represent some resources that can be copied, +// but either the source, destination, or both are not resources +// that support copying. +// +// - Performing the copy operation depended on a lower level operation +// such as network or filesystem access that has failed in some way. +// +// - If the scheme of the given URI does not have a registered +// CopyableRepository instance, then this method will fail with a +// repository.ErrOperationNotSupported. +// +// Copy is backed by the repository system - this function calls into a +// scheme-specific implementation from a registered repository. +// +// Since: 2.0 +func Copy(source fyne.URI, destination fyne.URI) error { + repo, err := repository.ForURI(source) + if err != nil { + return err + } + + crepo, ok := repo.(repository.CopyableRepository) + if !ok { + return repository.ErrOperationNotSupported + } + + return crepo.Copy(source, destination) +} + +// Move returns a method that given two URIs, 'src' and 'dest' both of the same +// scheme this will move src to dest. This means the resource referenced by +// src will be copied into the resource referenced by dest, and the resource +// referenced by src will no longer exist after the operation is complete. +// +// If the source and destination are of different schemes, then the Move +// implementation for the storage repository registered to the scheme of the +// source will be used. Implementations are recommended to use +// repository.GenericMove() as a fail-over in the case that they do not +// understand how to operate on the scheme of the destination URI. However, the +// behavior of calling Move() on URIs of non-matching schemes is ultimately +// defined by the storage repository registered to the scheme of the source +// URI. +// +// This method may fail in several ways: +// +// - Different permissions or credentials are required to perform the +// rename operation. +// +// - This URI scheme could represent some resources that can be renamed, +// but either the source, destination, or both are not resources +// that support renaming. +// +// - Performing the rename operation depended on a lower level operation +// such as network or filesystem access that has failed in some way. +// +// - If the scheme of the given URI does not have a registered +// MovableRepository instance, then this method will fail with a +// repository.ErrOperationNotSupported. +// +// Move is backed by the repository system - this function calls into a +// scheme-specific implementation from a registered repository. +// +// Since: 2.0 +func Move(source fyne.URI, destination fyne.URI) error { + repo, err := repository.ForURI(source) + if err != nil { + return err + } + + mrepo, ok := repo.(repository.MovableRepository) + if !ok { + return repository.ErrOperationNotSupported + } + + return mrepo.Move(source, destination) +} + +// CanList will determine if the URI is listable or not. +// +// This method may fail in several ways: +// +// - Different permissions or credentials are required to check if the +// URI supports listing. +// +// - This URI scheme could represent some resources that can be listed, +// but this specific URI is not one of them (e.g. a file on a +// filesystem, as opposed to a directory). +// +// - Checking for listability depended on a lower level operation +// such as network or filesystem access that has failed in some way. +// +// - If the scheme of the given URI does not have a registered +// ListableRepository instance, then this method will fail with a +// repository.ErrOperationNotSupported. +// +// CanList is backed by the repository system - this function calls into a +// scheme-specific implementation from a registered repository. +// +// Since: 2.0 +func CanList(u fyne.URI) (bool, error) { + repo, err := repository.ForURI(u) + if err != nil { + return false, err + } + + lrepo, ok := repo.(repository.ListableRepository) + if !ok { + return false, repository.ErrOperationNotSupported + } + + return lrepo.CanList(u) +} + +// List returns a list of URIs that reference resources which are nested below +// the resource referenced by the argument. For example, listing a directory on +// a filesystem should return a list of files and directories it contains. +// +// This method may fail in several ways: +// +// - Different permissions or credentials are required to obtain a +// listing for the given URI. +// +// - This URI scheme could represent some resources that can be listed, +// but this specific URI is not one of them (e.g. a file on a +// filesystem, as opposed to a directory). This can be tested in advance +// using the Listable() function. +// +// - Obtaining the listing depended on a lower level operation such as +// network or filesystem access that has failed in some way. +// +// - If the scheme of the given URI does not have a registered +// ListableRepository instance, then this method will fail with a +// repository.ErrOperationNotSupported. +// +// List is backed by the repository system - this function either calls into a +// scheme-specific implementation from a registered repository, or fails with a +// URIOperationNotSupported error. +// +// Since: 2.0 +func List(u fyne.URI) ([]fyne.URI, error) { + repo, err := repository.ForURI(u) + if err != nil { + return nil, err + } + + lrepo, ok := repo.(repository.ListableRepository) + if !ok { + return nil, repository.ErrOperationNotSupported + } + + return lrepo.List(u) +} + +// CreateListable creates a new listable resource referenced by the given URI. +// CreateListable will error if the URI already references an extant resource. +// This method is used for storage repositories where listable resources are of +// a different underlying type than other resources - for example, in a typical +// filesystem ('file://'), CreateListable() corresponds to directory creation, +// and Writer() implies file creation for non-extant operands. +// +// For storage repositories where listable and non-listable resources are the +// of the same underlying type, CreateListable should be equivalent to calling +// Writer(), writing zero bytes, and then closing the `URIWriteCloser - in +// filesystem terms, the same as calling 'touch;'. +// +// Storage repositories which support listing, but not creation of listable +// objects may return repository.ErrOperationNotSupported. +// +// CreateListable should generally fail if the parent of its operand does not +// exist, however this can vary by the implementation details of the specific +// storage repository. In filesystem terms, this function is "mkdir" not "mkdir +// -p". +// +// This method may fail in several ways: +// +// - Different permissions or credentials are required to create the requested +// resource. +// +// - Creating the resource depended on a lower level operation such as network +// or filesystem access that has failed in some way. +// +// - If the scheme of the given URI does not have a registered +// ListableRepository instance, then this method will fail with a +// repository.ErrOperationNotSupported. +// +// CreateListable is backed by the repository system - this function either +// calls into a scheme-specific implementation from a registered repository, or +// fails with a URIOperationNotSupported error. +// +// Since: 2.0 +func CreateListable(u fyne.URI) error { + repo, err := repository.ForURI(u) + if err != nil { + return err + } + + lrepo, ok := repo.(repository.ListableRepository) + if !ok { + return repository.ErrOperationNotSupported + } + + return lrepo.CreateListable(u) +} diff --git a/vendor/fyne.io/fyne/v2/storage/uri_root_error.go b/vendor/fyne.io/fyne/v2/storage/uri_root_error.go new file mode 100644 index 0000000..d397181 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/storage/uri_root_error.go @@ -0,0 +1,10 @@ +package storage + +import ( + "fyne.io/fyne/v2/storage/repository" +) + +// URIRootError is a wrapper for repository.URIRootError +// +// Deprecated - use repository.ErrURIRoot instead +var URIRootError = repository.ErrURIRoot diff --git a/vendor/fyne.io/fyne/v2/test/app.go b/vendor/fyne.io/fyne/v2/test/app.go new file mode 100644 index 0000000..f15130c --- /dev/null +++ b/vendor/fyne.io/fyne/v2/test/app.go @@ -0,0 +1,264 @@ +// Package test provides utility drivers for running UI tests without rendering to a screen. +package test // import "fyne.io/fyne/v2/test" + +import ( + "net/url" + "sync" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal" + intapp "fyne.io/fyne/v2/internal/app" + "fyne.io/fyne/v2/internal/cache" + "fyne.io/fyne/v2/internal/painter" + "fyne.io/fyne/v2/internal/test" + "fyne.io/fyne/v2/theme" +) + +// ensure we have a dummy app loaded and ready to test +func init() { + NewApp() +} + +type app struct { + driver *driver + settings *testSettings + prefs fyne.Preferences + propertyLock sync.RWMutex + storage fyne.Storage + lifecycle intapp.Lifecycle + clip fyne.Clipboard + cloud fyne.CloudProvider + + // user action variables + appliedTheme fyne.Theme + lastNotification *fyne.Notification +} + +func (a *app) CloudProvider() fyne.CloudProvider { + return a.cloud +} + +func (a *app) Icon() fyne.Resource { + return nil +} + +func (a *app) SetIcon(fyne.Resource) { + // no-op +} + +func (a *app) NewWindow(title string) fyne.Window { + return a.driver.CreateWindow(title) +} + +func (a *app) OpenURL(url *url.URL) error { + // no-op + return nil +} + +func (a *app) Run() { + // no-op +} + +func (a *app) Quit() { + // no-op +} + +func (a *app) Clipboard() fyne.Clipboard { + return a.clip +} + +func (a *app) UniqueID() string { + return "testApp" // TODO should this be randomised? +} + +func (a *app) Driver() fyne.Driver { + return a.driver +} + +func (a *app) SendNotification(notify *fyne.Notification) { + a.propertyLock.Lock() + defer a.propertyLock.Unlock() + + a.lastNotification = notify +} + +func (a *app) SetCloudProvider(p fyne.CloudProvider) { + if p == nil { + a.cloud = nil + return + } + + a.transitionCloud(p) +} + +func (a *app) Settings() fyne.Settings { + return a.settings +} + +func (a *app) Preferences() fyne.Preferences { + return a.prefs +} + +func (a *app) Storage() fyne.Storage { + return a.storage +} + +func (a *app) Lifecycle() fyne.Lifecycle { + return &a.lifecycle +} + +func (a *app) Metadata() fyne.AppMetadata { + return fyne.AppMetadata{} // just dummy data +} + +func (a *app) lastAppliedTheme() fyne.Theme { + a.propertyLock.Lock() + defer a.propertyLock.Unlock() + + return a.appliedTheme +} + +func (a *app) transitionCloud(p fyne.CloudProvider) { + if a.cloud != nil { + a.cloud.Cleanup(a) + } + + err := p.Setup(a) + if err != nil { + fyne.LogError("Failed to set up cloud provider "+p.ProviderName(), err) + return + } + a.cloud = p + + listeners := a.prefs.ChangeListeners() + if pp, ok := p.(fyne.CloudProviderPreferences); ok { + a.prefs = pp.CloudPreferences(a) + } else { + a.prefs = internal.NewInMemoryPreferences() + } + if store, ok := p.(fyne.CloudProviderStorage); ok { + a.storage = store.CloudStorage(a) + } else { + a.storage = &testStorage{} + } + + for _, l := range listeners { + a.prefs.AddChangeListener(l) + l() // assume that preferences have changed because we replaced the provider + } + + // after transition ensure settings listener is fired + a.settings.apply() +} + +// NewApp returns a new dummy app used for testing. +// It loads a test driver which creates a virtual window in memory for testing. +func NewApp() fyne.App { + settings := &testSettings{scale: 1.0, theme: Theme()} + prefs := internal.NewInMemoryPreferences() + store := &testStorage{} + test := &app{settings: settings, prefs: prefs, storage: store, driver: NewDriver().(*driver), clip: NewClipboard()} + settings.app = test + root, _ := store.docRootURI() + store.Docs = &internal.Docs{RootDocURI: root} + painter.ClearFontCache() + cache.ResetThemeCaches() + fyne.SetCurrentApp(test) + + return test +} + +type testSettings struct { + primaryColor string + scale float32 + theme fyne.Theme + + listeners []func(fyne.Settings) + changeListeners []chan fyne.Settings + propertyLock sync.RWMutex + app *app +} + +func (s *testSettings) AddChangeListener(listener chan fyne.Settings) { + s.propertyLock.Lock() + defer s.propertyLock.Unlock() + s.changeListeners = append(s.changeListeners, listener) +} + +func (s *testSettings) AddListener(listener func(fyne.Settings)) { + s.propertyLock.Lock() + defer s.propertyLock.Unlock() + s.listeners = append(s.listeners, listener) +} + +func (s *testSettings) BuildType() fyne.BuildType { + return fyne.BuildStandard +} + +func (s *testSettings) PrimaryColor() string { + if s.primaryColor != "" { + return s.primaryColor + } + + return theme.ColorBlue +} + +func (s *testSettings) SetTheme(theme fyne.Theme) { + s.propertyLock.Lock() + s.theme = theme + s.propertyLock.Unlock() + + s.apply() +} + +func (s *testSettings) ShowAnimations() bool { + return true +} + +func (s *testSettings) Theme() fyne.Theme { + s.propertyLock.RLock() + defer s.propertyLock.RUnlock() + + if s.theme == nil { + return test.DarkTheme(theme.DefaultTheme()) + } + + return s.theme +} + +func (s *testSettings) ThemeVariant() fyne.ThemeVariant { + return 2 // not a preference +} + +func (s *testSettings) Scale() float32 { + s.propertyLock.RLock() + defer s.propertyLock.RUnlock() + return s.scale +} + +func (s *testSettings) apply() { + s.propertyLock.RLock() + listeners := s.changeListeners + listenersFns := s.listeners + s.propertyLock.RUnlock() + + for _, listener := range listeners { + listener <- s + } + + s.app.driver.DoFromGoroutine(func() { + s.app.propertyLock.Lock() + painter.ClearFontCache() + cache.ResetThemeCaches() + intapp.ApplySettings(s, s.app) + s.app.propertyLock.Unlock() + + for _, l := range listenersFns { + l(s) + } + }, false) + + s.app.propertyLock.Lock() + s.app.appliedTheme = s.Theme() + s.app.propertyLock.Unlock() +} diff --git a/vendor/fyne.io/fyne/v2/test/app_helper.go b/vendor/fyne.io/fyne/v2/test/app_helper.go new file mode 100644 index 0000000..aa21a75 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/test/app_helper.go @@ -0,0 +1,18 @@ +//go:build !tamago && !noos + +package test + +import ( + "testing" + + "fyne.io/fyne/v2" +) + +// NewTempApp returns a new dummy app and tears it down at the end of the test. +// +// Since: 2.5 +func NewTempApp(t testing.TB) fyne.App { + app := NewApp() + t.Cleanup(func() { NewApp() }) + return app +} diff --git a/vendor/fyne.io/fyne/v2/test/canvas.go b/vendor/fyne.io/fyne/v2/test/canvas.go new file mode 100644 index 0000000..4cdc047 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/test/canvas.go @@ -0,0 +1,321 @@ +package test + +import ( + "image" + "image/draw" + "sync" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/driver/desktop" + "fyne.io/fyne/v2/internal" + intapp "fyne.io/fyne/v2/internal/app" + "fyne.io/fyne/v2/internal/cache" + "fyne.io/fyne/v2/internal/scale" + "fyne.io/fyne/v2/theme" +) + +var dummyCanvas WindowlessCanvas + +// WindowlessCanvas provides functionality for a canvas to operate without a window +type WindowlessCanvas interface { + fyne.Canvas + + Padded() bool + Resize(fyne.Size) + SetPadded(bool) + SetScale(float32) +} + +type canvas struct { + size fyne.Size + resized bool + scale float32 + + content fyne.CanvasObject + overlays internal.OverlayStack + focusMgr *intapp.FocusManager + hovered desktop.Hoverable + padded bool + transparent bool + + onTypedRune func(rune) + onTypedKey func(*fyne.KeyEvent) + + fyne.ShortcutHandler + painter SoftwarePainter + propertyLock sync.RWMutex +} + +// Canvas returns a reusable in-memory canvas used for testing +func Canvas() fyne.Canvas { + if dummyCanvas == nil { + dummyCanvas = NewCanvas() + } + + return dummyCanvas +} + +// NewCanvas returns a single use in-memory canvas used for testing. +// This canvas has no painter so calls to Capture() will return a blank image. +func NewCanvas() WindowlessCanvas { + c := &canvas{ + focusMgr: intapp.NewFocusManager(nil), + padded: true, + scale: 1.0, + size: fyne.NewSize(100, 100), + } + c.overlays.Canvas = c + return c +} + +// NewCanvasWithPainter allows creation of an in-memory canvas with a specific painter. +// The painter will be used to render in the Capture() call. +func NewCanvasWithPainter(painter SoftwarePainter) WindowlessCanvas { + c := NewCanvas().(*canvas) + c.painter = painter + + return c +} + +// NewTransparentCanvasWithPainter allows creation of an in-memory canvas with a specific painter without a background color. +// The painter will be used to render in the Capture() call. +// +// Since: 2.2 +func NewTransparentCanvasWithPainter(painter SoftwarePainter) WindowlessCanvas { + c := NewCanvasWithPainter(painter).(*canvas) + c.transparent = true + + return c +} + +func (c *canvas) Capture() image.Image { + cache.Clean(true) + size := c.Size() + bounds := image.Rect(0, 0, scale.ToScreenCoordinate(c, size.Width), scale.ToScreenCoordinate(c, size.Height)) + img := image.NewNRGBA(bounds) + if !c.transparent { + draw.Draw(img, bounds, image.NewUniform(theme.Color(theme.ColorNameBackground)), image.Point{}, draw.Src) + } + + if c.painter != nil { + draw.Draw(img, bounds, c.painter.Paint(c), image.Point{}, draw.Over) + } + + return img +} + +func (c *canvas) Content() fyne.CanvasObject { + c.propertyLock.RLock() + defer c.propertyLock.RUnlock() + + return c.content +} + +func (c *canvas) Focus(obj fyne.Focusable) { + c.focusManager().Focus(obj) +} + +func (c *canvas) FocusNext() { + c.focusManager().FocusNext() +} + +func (c *canvas) FocusPrevious() { + c.focusManager().FocusPrevious() +} + +func (c *canvas) Focused() fyne.Focusable { + return c.focusManager().Focused() +} + +func (c *canvas) InteractiveArea() (fyne.Position, fyne.Size) { + return fyne.NewPos(2, 3), c.Size().SubtractWidthHeight(4, 5) +} + +func (c *canvas) OnTypedKey() func(*fyne.KeyEvent) { + c.propertyLock.RLock() + defer c.propertyLock.RUnlock() + + return c.onTypedKey +} + +func (c *canvas) OnTypedRune() func(rune) { + c.propertyLock.RLock() + defer c.propertyLock.RUnlock() + + return c.onTypedRune +} + +func (c *canvas) Overlays() fyne.OverlayStack { + c.propertyLock.Lock() + defer c.propertyLock.Unlock() + + return &c.overlays +} + +func (c *canvas) Padded() bool { + c.propertyLock.RLock() + defer c.propertyLock.RUnlock() + + return c.padded +} + +func (c *canvas) PixelCoordinateForPosition(pos fyne.Position) (int, int) { + return int(pos.X * c.scale), int(pos.Y * c.scale) +} + +func (c *canvas) Refresh(fyne.CanvasObject) { +} + +func (c *canvas) Resize(size fyne.Size) { + c.propertyLock.Lock() + c.resized = true + c.propertyLock.Unlock() + + c.doResize(size) +} + +func (c *canvas) doResize(size fyne.Size) { + c.propertyLock.Lock() + content := c.content + overlays := c.overlays + padded := c.padded + c.size = size + c.propertyLock.Unlock() + + if content == nil { + return + } + + // Ensure testcanvas mimics real canvas.Resize behavior + for _, overlay := range overlays.List() { + type popupWidget interface { + fyne.CanvasObject + ShowAtPosition(fyne.Position) + } + if p, ok := overlay.(popupWidget); ok { + // TODO: remove this when #707 is being addressed. + // “Notifies” the PopUp of the canvas size change. + p.Refresh() + } else { + overlay.Resize(size) + } + } + + if padded { + padding := theme.Padding() + content.Resize(size.Subtract(fyne.NewSquareSize(padding * 2))) + content.Move(fyne.NewSquareOffsetPos(padding)) + } else { + content.Resize(size) + content.Move(fyne.NewPos(0, 0)) + } +} + +func (c *canvas) Scale() float32 { + c.propertyLock.RLock() + defer c.propertyLock.RUnlock() + + return c.scale +} + +func (c *canvas) SetContent(content fyne.CanvasObject) { + c.propertyLock.Lock() + c.content = content + c.focusMgr = intapp.NewFocusManager(c.content) + resized := c.resized + c.propertyLock.Unlock() + + if content == nil { + return + } + + minSize := content.MinSize() + if c.padded { + minSize = minSize.Add(fyne.NewSquareSize(theme.Padding() * 2)) + } + + if resized { + c.doResize(c.Size().Max(minSize)) + } else { + c.doResize(minSize) + } +} + +func (c *canvas) SetOnTypedKey(handler func(*fyne.KeyEvent)) { + c.propertyLock.Lock() + defer c.propertyLock.Unlock() + + c.onTypedKey = handler +} + +func (c *canvas) SetOnTypedRune(handler func(rune)) { + c.propertyLock.Lock() + defer c.propertyLock.Unlock() + + c.onTypedRune = handler +} + +func (c *canvas) SetPadded(padded bool) { + c.propertyLock.Lock() + c.padded = padded + c.propertyLock.Unlock() + + c.doResize(c.Size()) +} + +func (c *canvas) SetScale(scale float32) { + c.propertyLock.Lock() + defer c.propertyLock.Unlock() + + c.scale = scale +} + +func (c *canvas) Size() fyne.Size { + c.propertyLock.RLock() + defer c.propertyLock.RUnlock() + + return c.size +} + +func (c *canvas) Unfocus() { + c.focusManager().Focus(nil) +} + +func (c *canvas) focusManager() *intapp.FocusManager { + c.propertyLock.RLock() + defer c.propertyLock.RUnlock() + if focusMgr := c.overlays.TopFocusManager(); focusMgr != nil { + return focusMgr + } + return c.focusMgr +} + +func (c *canvas) objectTrees() []fyne.CanvasObject { + overlays := c.Overlays().List() + trees := make([]fyne.CanvasObject, 0, len(overlays)+1) + if c.content != nil { + trees = append(trees, c.content) + } + trees = append(trees, overlays...) + return trees +} + +func layoutAndCollect(objects []fyne.CanvasObject, o fyne.CanvasObject, size fyne.Size) []fyne.CanvasObject { + objects = append(objects, o) + switch c := o.(type) { + case fyne.Widget: + r := c.CreateRenderer() + r.Layout(size) + for _, child := range r.Objects() { + objects = layoutAndCollect(objects, child, child.Size()) + } + case *fyne.Container: + if c.Layout != nil { + c.Layout.Layout(c.Objects, size) + } + for _, child := range c.Objects { + objects = layoutAndCollect(objects, child, child.Size()) + } + } + return objects +} diff --git a/vendor/fyne.io/fyne/v2/test/clipboard.go b/vendor/fyne.io/fyne/v2/test/clipboard.go new file mode 100644 index 0000000..db37f3c --- /dev/null +++ b/vendor/fyne.io/fyne/v2/test/clipboard.go @@ -0,0 +1,20 @@ +package test + +import "fyne.io/fyne/v2" + +type clipboard struct { + content string +} + +func (c *clipboard) Content() string { + return c.content +} + +func (c *clipboard) SetContent(content string) { + c.content = content +} + +// NewClipboard returns a single use in-memory clipboard used for testing +func NewClipboard() fyne.Clipboard { + return &clipboard{} +} diff --git a/vendor/fyne.io/fyne/v2/test/cloud.go b/vendor/fyne.io/fyne/v2/test/cloud.go new file mode 100644 index 0000000..8295b35 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/test/cloud.go @@ -0,0 +1,31 @@ +package test + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/theme" +) + +type mockCloud struct { + configured bool +} + +func (c *mockCloud) Cleanup(_ fyne.App) { + c.configured = false +} + +func (c *mockCloud) ProviderDescription() string { + return "Mock cloud implementation" +} + +func (c *mockCloud) ProviderIcon() fyne.Resource { + return theme.ComputerIcon() +} + +func (c *mockCloud) ProviderName() string { + return "mock" +} + +func (c *mockCloud) Setup(_ fyne.App) error { + c.configured = true + return nil +} diff --git a/vendor/fyne.io/fyne/v2/test/device.go b/vendor/fyne.io/fyne/v2/test/device.go new file mode 100644 index 0000000..740766e --- /dev/null +++ b/vendor/fyne.io/fyne/v2/test/device.go @@ -0,0 +1,36 @@ +package test + +import ( + "runtime" + + "fyne.io/fyne/v2" +) + +type device struct{} + +// Declare conformity with Device +var _ fyne.Device = (*device)(nil) + +func (d *device) Orientation() fyne.DeviceOrientation { + return fyne.OrientationVertical +} + +func (d *device) HasKeyboard() bool { + return false +} + +func (d *device) SystemScale() float32 { + return d.SystemScaleForWindow(nil) +} + +func (d *device) SystemScaleForWindow(fyne.Window) float32 { + return 1 +} + +func (d *device) Locale() fyne.Locale { + return "en" +} + +func (*device) IsBrowser() bool { + return runtime.GOARCH == "js" || runtime.GOOS == "js" +} diff --git a/vendor/fyne.io/fyne/v2/test/device_mobile.go b/vendor/fyne.io/fyne/v2/test/device_mobile.go new file mode 100644 index 0000000..781f0f7 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/test/device_mobile.go @@ -0,0 +1,7 @@ +//go:build mobile + +package test + +func (d *device) IsMobile() bool { + return true +} diff --git a/vendor/fyne.io/fyne/v2/test/device_other.go b/vendor/fyne.io/fyne/v2/test/device_other.go new file mode 100644 index 0000000..23b80f7 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/test/device_other.go @@ -0,0 +1,7 @@ +//go:build !mobile + +package test + +func (d *device) IsMobile() bool { + return false +} diff --git a/vendor/fyne.io/fyne/v2/test/driver.go b/vendor/fyne.io/fyne/v2/test/driver.go new file mode 100644 index 0000000..b116955 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/test/driver.go @@ -0,0 +1,153 @@ +package test + +import ( + "image" + "sync" + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/async" + intdriver "fyne.io/fyne/v2/internal/driver" + "fyne.io/fyne/v2/internal/painter" + "fyne.io/fyne/v2/internal/painter/software" + intRepo "fyne.io/fyne/v2/internal/repository" + "fyne.io/fyne/v2/storage/repository" +) + +// SoftwarePainter describes a simple type that can render canvases +type SoftwarePainter interface { + Paint(fyne.Canvas) image.Image +} + +type driver struct { + device device + painter SoftwarePainter + windows []fyne.Window + windowsMutex sync.RWMutex +} + +// Declare conformity with Driver +var _ fyne.Driver = (*driver)(nil) + +// NewDriver sets up and registers a new dummy driver for test purpose +func NewDriver() fyne.Driver { + drv := &driver{windowsMutex: sync.RWMutex{}} + repository.Register("file", intRepo.NewFileRepository()) + + httpHandler := intRepo.NewHTTPRepository() + repository.Register("http", httpHandler) + repository.Register("https", httpHandler) + + // make a single dummy window for rendering tests + drv.CreateWindow("") + + return drv +} + +// NewDriverWithPainter creates a new dummy driver that will pass the given +// painter to all canvases created +func NewDriverWithPainter(painter SoftwarePainter) fyne.Driver { + return &driver{painter: painter} +} + +// DoFromGoroutine on a test driver ignores the wait flag as our threading is simple +func (d *driver) DoFromGoroutine(f func(), _ bool) { + // Tests all run on a single (but potentially different per-test) thread + async.EnsureNotMain(f) +} + +func (d *driver) AbsolutePositionForObject(co fyne.CanvasObject) fyne.Position { + c := d.CanvasForObject(co) + if c == nil { + return fyne.NewPos(0, 0) + } + + tc := c.(*canvas) + pos := intdriver.AbsolutePositionForObject(co, tc.objectTrees()) + inset, _ := c.InteractiveArea() + return pos.Subtract(inset) +} + +func (d *driver) AllWindows() []fyne.Window { + d.windowsMutex.RLock() + defer d.windowsMutex.RUnlock() + return d.windows +} + +func (d *driver) CanvasForObject(fyne.CanvasObject) fyne.Canvas { + d.windowsMutex.RLock() + defer d.windowsMutex.RUnlock() + // cheating: probably the last created window is meant + return d.windows[len(d.windows)-1].Canvas() +} + +func (d *driver) CreateWindow(title string) fyne.Window { + c := NewCanvas().(*canvas) + if d.painter != nil { + c.painter = d.painter + } else { + c.painter = software.NewPainter() + } + + w := &window{canvas: c, driver: d, title: title} + + d.windowsMutex.Lock() + d.windows = append(d.windows, w) + d.windowsMutex.Unlock() + return w +} + +func (d *driver) Device() fyne.Device { + return &d.device +} + +// RenderedTextSize looks up how bit a string would be if drawn on screen +func (d *driver) RenderedTextSize(text string, size float32, style fyne.TextStyle, source fyne.Resource) (fyne.Size, float32) { + return painter.RenderedTextSize(text, size, style, source) +} + +func (d *driver) Run() { + // no-op +} + +func (d *driver) StartAnimation(a *fyne.Animation) { + // currently no animations in test app, we just initialise it and leave + a.Tick(1.0) +} + +func (d *driver) StopAnimation(a *fyne.Animation) { + // currently no animations in test app, do nothing +} + +func (d *driver) Quit() { + // no-op +} + +func (d *driver) Clipboard() fyne.Clipboard { + return nil +} + +func (d *driver) removeWindow(w *window) { + d.windowsMutex.Lock() + i := 0 + for _, win := range d.windows { + if win == w { + break + } + i++ + } + + copy(d.windows[i:], d.windows[i+1:]) + d.windows[len(d.windows)-1] = nil // Allow the garbage collector to reclaim the memory. + d.windows = d.windows[:len(d.windows)-1] + + d.windowsMutex.Unlock() +} + +func (d *driver) DoubleTapDelay() time.Duration { + return 300 * time.Millisecond +} + +func (d *driver) SetDisableScreenBlanking(_ bool) { + // no-op for test +} diff --git a/vendor/fyne.io/fyne/v2/test/file.go b/vendor/fyne.io/fyne/v2/test/file.go new file mode 100644 index 0000000..6aca1bf --- /dev/null +++ b/vendor/fyne.io/fyne/v2/test/file.go @@ -0,0 +1,106 @@ +package test + +import ( + "errors" + "fmt" + "io" + "os" + "path/filepath" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/storage" +) + +var errUnsupportedURLProtocol = errors.New("unsupported URL protocol") + +type file struct { + *os.File + path string +} + +type directory struct { + fyne.URI +} + +// Declare conformity to the ListableURI interface +var _ fyne.ListableURI = (*directory)(nil) + +func (f *file) Open() (io.ReadCloser, error) { + return os.Open(f.path) +} + +func (f *file) Save() (io.WriteCloser, error) { + return os.Open(f.path) +} + +func (f *file) ReadOnly() bool { + return true +} + +func (f *file) Name() string { + return filepath.Base(f.path) +} + +func (f *file) URI() fyne.URI { + return storage.NewFileURI(f.path) +} + +func openFile(uri fyne.URI, create bool) (*file, error) { + if uri.Scheme() != "file" { + return nil, errUnsupportedURLProtocol + } + + path := uri.Path() + if create { + f, err := os.Create(path) + return &file{File: f, path: path}, err + } + + f, err := os.Open(path) + return &file{File: f, path: path}, err +} + +func (d *driver) FileReaderForURI(uri fyne.URI) (fyne.URIReadCloser, error) { + return openFile(uri, false) +} + +func (d *driver) FileWriterForURI(uri fyne.URI) (fyne.URIWriteCloser, error) { + return openFile(uri, true) +} + +func (d *driver) ListerForURI(uri fyne.URI) (fyne.ListableURI, error) { + if uri.Scheme() != "file" { + return nil, errUnsupportedURLProtocol + } + + path := uri.Path() + s, err := os.Stat(path) + if err != nil { + return nil, err + } + + if !s.IsDir() { + return nil, fmt.Errorf("path '%s' is not a directory, cannot convert to listable URI", path) + } + + return &directory{URI: uri}, nil +} + +func (d *directory) List() ([]fyne.URI, error) { + if d.Scheme() != "file" { + return nil, errUnsupportedURLProtocol + } + + path := d.Path() + files, err := os.ReadDir(path) + if err != nil { + return nil, err + } + + urilist := make([]fyne.URI, len(files)) + for i, f := range files { + urilist[i] = storage.NewFileURI(filepath.Join(path, f.Name())) + } + + return urilist, nil +} diff --git a/vendor/fyne.io/fyne/v2/test/markup_renderer.go b/vendor/fyne.io/fyne/v2/test/markup_renderer.go new file mode 100644 index 0000000..645ba6d --- /dev/null +++ b/vendor/fyne.io/fyne/v2/test/markup_renderer.go @@ -0,0 +1,658 @@ +package test + +import ( + "fmt" + "image/color" + "reflect" + "sort" + "strings" + "unsafe" + + "fyne.io/fyne/v2" + fynecanvas "fyne.io/fyne/v2/canvas" + col "fyne.io/fyne/v2/internal/color" + intdriver "fyne.io/fyne/v2/internal/driver" + "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/theme" +) + +type markupRenderer struct { + indentation int + w strings.Builder +} + +// snapshot creates a new snapshot of the current render tree. +func snapshot(c fyne.Canvas) string { + r := markupRenderer{} + r.writeCanvas(c) + return r.w.String() +} + +func (r *markupRenderer) setAlignmentAttr(attrs map[string]*string, name string, a fyne.TextAlign) { + var value string + switch a { + case fyne.TextAlignLeading: + // default mode, don’t add an attr + case fyne.TextAlignCenter: + value = "center" + case fyne.TextAlignTrailing: + value = "trailing" + default: + value = fmt.Sprintf("unknown alignment: %d", a) + } + r.setStringAttr(attrs, name, value) +} + +func (r *markupRenderer) setBoolAttr(attrs map[string]*string, name string, b bool) { + if !b { + return + } + attrs[name] = nil +} + +func (r *markupRenderer) setColorAttr(attrs map[string]*string, name string, c color.Color) { + r.setColorAttrWithDefault(attrs, name, c, color.Transparent) +} + +func (r *markupRenderer) setColorAttrWithDefault(attrs map[string]*string, name string, c color.Color, d color.Color) { + if c == nil || c == d { + return + } + + if value := knownColor(c); value != "" { + r.setStringAttr(attrs, name, value) + return + } + + rd, g, b, a := col.ToNRGBA(c) + r.setStringAttr(attrs, name, fmt.Sprintf("rgba(%d,%d,%d,%d)", uint8(rd), uint8(g), uint8(b), uint8(a))) +} + +func (r *markupRenderer) setFillModeAttr(attrs map[string]*string, name string, m fynecanvas.ImageFill) { + var fillMode string + switch m { + case fynecanvas.ImageFillStretch: + // default mode, don’t add an attr + case fynecanvas.ImageFillContain: + fillMode = "contain" + case fynecanvas.ImageFillOriginal: + fillMode = "original" + default: + fillMode = fmt.Sprintf("unknown fill mode: %d", m) + } + r.setStringAttr(attrs, name, fillMode) +} + +func (r *markupRenderer) setFloatAttr(attrs map[string]*string, name string, f float64) { + r.setFloatAttrWithDefault(attrs, name, f, 0) +} + +func (r *markupRenderer) setFloatAttrWithDefault(attrs map[string]*string, name string, f float64, d float64) { + if f == d { + return + } + value := fmt.Sprintf("%g", f) + attrs[name] = &value +} + +func (r *markupRenderer) setFloatPosAttr(attrs map[string]*string, name string, x, y float64) { + if x == 0 && y == 0 { + return + } + value := fmt.Sprintf("%g,%g", x, y) + attrs[name] = &value +} + +func (r *markupRenderer) setSizeAttrWithDefault(attrs map[string]*string, name string, i float32, d float32) { + if int(i) == int(d) { + return + } + value := fmt.Sprintf("%d", int(i)) + attrs[name] = &value +} + +func (r *markupRenderer) setPosAttr(attrs map[string]*string, name string, pos fyne.Position) { + if int(pos.X) == 0 && int(pos.Y) == 0 { + return + } + value := fmt.Sprintf("%d,%d", int(pos.X), int(pos.Y)) + attrs[name] = &value +} + +func (r *markupRenderer) setResourceAttr(attrs map[string]*string, name string, rsc fyne.Resource) { + if rsc == nil { + return + } + + named := false + if value := knownResource(rsc); value != "" { + r.setStringAttr(attrs, name, value) + named = true + } + + var variant string + switch t := rsc.(type) { + case *theme.DisabledResource: + variant = "disabled" + case *theme.ErrorThemedResource: + variant = "error" + case *theme.InvertedThemedResource: + variant = "inverted" + case *theme.PrimaryThemedResource: + variant = "primary" + case *theme.ThemedResource: + variant = string(t.ColorName) + if variant == "" { + variant = "foreground" + } + default: + r.setStringAttr(attrs, name, rsc.Name()) + return + } + + if !named { + // That’s some magic to access the private `source` field of the themed resource. + v := reflect.ValueOf(rsc).Elem().Field(0) + src := reflect.NewAt(v.Type(), unsafe.Pointer(v.UnsafeAddr())).Elem().Interface().(fyne.Resource) + r.setResourceAttr(attrs, name, src) + } + r.setStringAttr(attrs, "themed", variant) +} + +func (r *markupRenderer) setScaleModeAttr(attrs map[string]*string, name string, m fynecanvas.ImageScale) { + var scaleMode string + switch m { + case fynecanvas.ImageScaleSmooth: + // default mode, don’t add an attr + case fynecanvas.ImageScalePixels: + scaleMode = "pixels" + default: + scaleMode = fmt.Sprintf("unknown scale mode: %d", m) + } + r.setStringAttr(attrs, name, scaleMode) +} + +func (r *markupRenderer) setSizeAttr(attrs map[string]*string, name string, size fyne.Size) { + value := fmt.Sprintf("%dx%d", int(size.Width), int(size.Height)) + attrs[name] = &value +} + +func (r *markupRenderer) setStringAttr(attrs map[string]*string, name string, s string) { + if s == "" { + return + } + attrs[name] = &s +} + +func (r *markupRenderer) writeCanvas(c fyne.Canvas) { + attrs := map[string]*string{} + r.setSizeAttr(attrs, "size", c.Size()) + if tc, ok := c.(WindowlessCanvas); ok { + r.setBoolAttr(attrs, "padded", tc.Padded()) + } + r.writeTag("canvas", false, attrs) + r.w.WriteRune('\n') + r.indentation++ + r.writeTag("content", false, nil) + r.w.WriteRune('\n') + r.indentation++ + intdriver.WalkVisibleObjectTree(c.Content(), r.writeCanvasObject, r.writeCloseCanvasObject) + r.indentation-- + r.writeIndent() + r.writeCloseTag("content") + for _, o := range c.Overlays().List() { + r.writeTag("overlay", false, nil) + r.w.WriteRune('\n') + r.indentation++ + intdriver.WalkVisibleObjectTree(o, r.writeCanvasObject, r.writeCloseCanvasObject) + r.indentation-- + r.writeIndent() + r.writeCloseTag("overlay") + } + r.indentation-- + r.writeIndent() + r.writeCloseTag("canvas") +} + +func (r *markupRenderer) writeCanvasObject(obj fyne.CanvasObject, _, _ fyne.Position, _ fyne.Size) bool { + attrs := map[string]*string{} + r.setPosAttr(attrs, "pos", obj.Position()) + r.setSizeAttr(attrs, "size", obj.Size()) + switch o := obj.(type) { + case *fynecanvas.Circle: + r.writeCircle(o, attrs) + case *fynecanvas.Image: + r.writeImage(o, attrs) + case *fynecanvas.Line: + r.writeLine(o, attrs) + case *fynecanvas.LinearGradient: + r.writeLinearGradient(o, attrs) + case *fynecanvas.RadialGradient: + r.writeRadialGradient(o, attrs) + case *fynecanvas.Raster: + r.writeRaster(o, attrs) + case *fynecanvas.Polygon: + r.writePolygon(o, attrs) + case *fynecanvas.Rectangle: + r.writeRectangle(o, attrs) + case *fynecanvas.Text: + r.writeText(o, attrs) + case *fyne.Container: + r.writeContainer(o, attrs) + case fyne.Widget: + r.writeWidget(o, attrs) + case *layout.Spacer: + r.writeSpacer(o, attrs) + case *fynecanvas.Arc: + r.writeArc(o, attrs) + default: + panic(fmt.Sprint("please add support for", reflect.TypeOf(o))) + } + + return false +} + +func (r *markupRenderer) writeArc(a *fynecanvas.Arc, attrs map[string]*string) { + r.setColorAttr(attrs, "fillColor", a.FillColor) + r.setFloatAttr(attrs, "cutoutRatio", float64(a.CutoutRatio)) + r.setFloatAttr(attrs, "startAngle", float64(a.StartAngle)) + r.setFloatAttr(attrs, "endAngle", float64(a.EndAngle)) + r.setFloatAttr(attrs, "radius", float64(a.CornerRadius)) + r.setColorAttr(attrs, "strokeColor", a.StrokeColor) + r.setFloatAttr(attrs, "strokeWidth", float64(a.StrokeWidth)) + r.writeTag("arc", true, attrs) +} + +func (r *markupRenderer) writeCircle(c *fynecanvas.Circle, attrs map[string]*string) { + r.setColorAttr(attrs, "fillColor", c.FillColor) + r.setColorAttr(attrs, "strokeColor", c.StrokeColor) + r.setFloatAttr(attrs, "strokeWidth", float64(c.StrokeWidth)) + r.writeTag("circle", true, attrs) +} + +func (r *markupRenderer) writeCloseCanvasObject(o fyne.CanvasObject, _ fyne.Position, _ fyne.CanvasObject) { + switch o.(type) { + case *fyne.Container: + r.indentation-- + r.writeIndent() + r.writeCloseTag("container") + case fyne.Widget: + r.indentation-- + r.writeIndent() + r.writeCloseTag("widget") + } +} + +func (r *markupRenderer) writeCloseTag(name string) { + r.w.WriteString("\n") +} + +func (r *markupRenderer) writeContainer(_ *fyne.Container, attrs map[string]*string) { + r.writeTag("container", false, attrs) + r.w.WriteRune('\n') + r.indentation++ +} + +func (r *markupRenderer) writeIndent() { + for i := 0; i < r.indentation; i++ { + r.w.WriteRune('\t') + } +} + +func (r *markupRenderer) writeImage(i *fynecanvas.Image, attrs map[string]*string) { + r.setStringAttr(attrs, "file", i.File) + r.setResourceAttr(attrs, "rsc", i.Resource) + if i.File == "" && i.Resource == nil { + r.setBoolAttr(attrs, "img", i.Image != nil) + } + r.setFloatAttr(attrs, "translucency", i.Translucency) + r.setFillModeAttr(attrs, "fillMode", i.FillMode) + r.setScaleModeAttr(attrs, "scaleMode", i.ScaleMode) + if i.Size().Width == theme.IconInlineSize() && i.Size().Height == i.Size().Width { + r.setStringAttr(attrs, "size", "iconInlineSize") + } + r.writeTag("image", true, attrs) +} + +func (r *markupRenderer) writeLine(l *fynecanvas.Line, attrs map[string]*string) { + r.setColorAttr(attrs, "strokeColor", l.StrokeColor) + r.setFloatAttrWithDefault(attrs, "strokeWidth", float64(l.StrokeWidth), 1) + r.writeTag("line", true, attrs) +} + +func (r *markupRenderer) writeLinearGradient(g *fynecanvas.LinearGradient, attrs map[string]*string) { + r.setColorAttr(attrs, "startColor", g.StartColor) + r.setColorAttr(attrs, "endColor", g.EndColor) + r.setFloatAttr(attrs, "angle", g.Angle) + r.writeTag("linearGradient", true, attrs) +} + +func (r *markupRenderer) writeRadialGradient(g *fynecanvas.RadialGradient, attrs map[string]*string) { + r.setColorAttr(attrs, "startColor", g.StartColor) + r.setColorAttr(attrs, "endColor", g.EndColor) + r.setFloatPosAttr(attrs, "centerOffset", g.CenterOffsetX, g.CenterOffsetY) + r.writeTag("radialGradient", true, attrs) +} + +func (r *markupRenderer) writeRaster(rst *fynecanvas.Raster, attrs map[string]*string) { + r.setFloatAttr(attrs, "translucency", rst.Translucency) + r.writeTag("raster", true, attrs) +} + +func (r *markupRenderer) writePolygon(rct *fynecanvas.Polygon, attrs map[string]*string) { + r.setColorAttr(attrs, "fillColor", rct.FillColor) + r.setColorAttr(attrs, "strokeColor", rct.StrokeColor) + r.setFloatAttr(attrs, "strokeWidth", float64(rct.StrokeWidth)) + r.setFloatAttr(attrs, "radius", float64(rct.CornerRadius)) + r.setFloatAttr(attrs, "angle", float64(rct.Angle)) + r.setFloatAttr(attrs, "sides", float64(rct.Sides)) + r.writeTag("polygon", true, attrs) +} + +func (r *markupRenderer) writeRectangle(rct *fynecanvas.Rectangle, attrs map[string]*string) { + r.setColorAttr(attrs, "fillColor", rct.FillColor) + r.setColorAttr(attrs, "strokeColor", rct.StrokeColor) + r.setFloatAttr(attrs, "strokeWidth", float64(rct.StrokeWidth)) + r.setFloatAttr(attrs, "radius", float64(rct.CornerRadius)) + r.setFloatAttr(attrs, "aspect", float64(rct.Aspect)) + r.setFloatAttr(attrs, "topRightRadius", float64(rct.TopRightCornerRadius)) + r.setFloatAttr(attrs, "topLeftRadius", float64(rct.TopLeftCornerRadius)) + r.setFloatAttr(attrs, "bottomRightRadius", float64(rct.BottomRightCornerRadius)) + r.setFloatAttr(attrs, "bottomLeftRadius", float64(rct.BottomLeftCornerRadius)) + r.writeTag("rectangle", true, attrs) +} + +func (r *markupRenderer) writeSpacer(_ *layout.Spacer, attrs map[string]*string) { + r.writeTag("spacer", true, attrs) +} + +func (r *markupRenderer) writeTag(name string, isEmpty bool, attrs map[string]*string) { + r.writeIndent() + r.w.WriteRune('<') + r.w.WriteString(name) + for _, key := range sortedKeys(attrs) { + r.w.WriteRune(' ') + r.w.WriteString(key) + if attrs[key] != nil { + r.w.WriteString("=\"") + r.w.WriteString(*attrs[key]) + r.w.WriteRune('"') + } + + } + if isEmpty { + r.w.WriteString("/>\n") + } else { + r.w.WriteRune('>') + } +} + +func (r *markupRenderer) writeText(t *fynecanvas.Text, attrs map[string]*string) { + r.setColorAttrWithDefault(attrs, "color", t.Color, theme.Color(theme.ColorNameForeground)) + r.setAlignmentAttr(attrs, "alignment", t.Alignment) + r.setSizeAttrWithDefault(attrs, "textSize", t.TextSize, theme.TextSize()) + r.setBoolAttr(attrs, "bold", t.TextStyle.Bold) + r.setBoolAttr(attrs, "italic", t.TextStyle.Italic) + r.setBoolAttr(attrs, "monospace", t.TextStyle.Monospace) + r.writeTag("text", false, attrs) + r.w.WriteString(t.Text) + r.writeCloseTag("text") +} + +func (r *markupRenderer) writeWidget(w fyne.Widget, attrs map[string]*string) { + r.setStringAttr(attrs, "type", reflect.TypeOf(w).String()) + r.writeTag("widget", false, attrs) + r.w.WriteRune('\n') + r.indentation++ +} + +func nrgbaColor(c color.Color) color.NRGBA { + // using ColorToNRGBA to avoid problems with colors with 16-bit components or alpha values that aren't 0 or the maximum possible alpha value + r, g, b, a := col.ToNRGBA(c) + return color.NRGBA{R: uint8(r), G: uint8(g), B: uint8(b), A: uint8(a)} +} + +//gocyclo:ignore +func knownColor(c color.Color) string { + switch nrgbaColor(c) { + case nrgbaColor(theme.Color(theme.ColorNameBackground)): + return "background" + case nrgbaColor(theme.Color(theme.ColorNameButton)): + return "button" + case nrgbaColor(theme.Color(theme.ColorNameDisabledButton)): + return "disabled button" + case nrgbaColor(theme.Color(theme.ColorNameDisabled)): + return "disabled" + case nrgbaColor(theme.Color(theme.ColorNameError)): + return "error" + case nrgbaColor(theme.Color(theme.ColorNameFocus)): + return "focus" + case nrgbaColor(theme.Color(theme.ColorNameForeground)): + return "foreground" + case nrgbaColor(theme.Color(theme.ColorNameForegroundOnError)): + return "foregroundOnError" + case nrgbaColor(theme.Color(theme.ColorNameForegroundOnPrimary)): + return "foregroundOnPrimary" + case nrgbaColor(theme.Color(theme.ColorNameForegroundOnSuccess)): + return "foregroundOnSuccess" + case nrgbaColor(theme.Color(theme.ColorNameForegroundOnWarning)): + return "foregroundOnWarning" + case nrgbaColor(theme.Color(theme.ColorNameHeaderBackground)): + return "headerBackground" + case nrgbaColor(theme.Color(theme.ColorNameHover)): + return "hover" + case nrgbaColor(theme.Color(theme.ColorNameHyperlink)): + return "hyperlink" + case nrgbaColor(theme.Color(theme.ColorNameInputBackground)): + return "inputBackground" + case nrgbaColor(theme.Color(theme.ColorNameInputBorder)): + return "inputBorder" + case nrgbaColor(theme.Color(theme.ColorNameMenuBackground)): + return "menuBackground" + case nrgbaColor(theme.Color(theme.ColorNameOverlayBackground)): + return "overlayBackground" + case nrgbaColor(theme.Color(theme.ColorNamePlaceHolder)): + return "placeholder" + case nrgbaColor(theme.Color(theme.ColorNamePressed)): + return "pressed" + case nrgbaColor(theme.Color(theme.ColorNamePrimary)): + return "primary" + case nrgbaColor(theme.Color(theme.ColorNameScrollBar)): + return "scrollbar" + case nrgbaColor(theme.Color(theme.ColorNameScrollBarBackground)): + return "scrollbarBackground" + case nrgbaColor(theme.Color(theme.ColorNameSelection)): + return "selection" + case nrgbaColor(theme.Color(theme.ColorNameSeparator)): + return "separator" + case nrgbaColor(theme.Color(theme.ColorNameSuccess)): + return "success" + case nrgbaColor(theme.Color(theme.ColorNameShadow)): + return "shadow" + case nrgbaColor(theme.Color(theme.ColorNameWarning)): + return "warning" + default: + return "" + } +} + +//gocyclo:ignore +func knownResource(rsc fyne.Resource) string { + switch rsc { + case theme.CancelIcon(): + return "cancelIcon" + case theme.CheckButtonCheckedIcon(): + return "checkButtonCheckedIcon" + case theme.CheckButtonFillIcon(): + return "checkButtonFillIcon" + case theme.CheckButtonIcon(): + return "checkButtonIcon" + case theme.ColorAchromaticIcon(): + return "colorAchromaticIcon" + case theme.ColorChromaticIcon(): + return "colorChromaticIcon" + case theme.ColorPaletteIcon(): + return "colorPaletteIcon" + case theme.ComputerIcon(): + return "computerIcon" + case theme.ConfirmIcon(): + return "confirmIcon" + case theme.ContentAddIcon(): + return "contentAddIcon" + case theme.ContentClearIcon(): + return "contentClearIcon" + case theme.ContentCopyIcon(): + return "contentCopyIcon" + case theme.ContentCutIcon(): + return "contentCutIcon" + case theme.ContentPasteIcon(): + return "contentPasteIcon" + case theme.ContentRedoIcon(): + return "contentRedoIcon" + case theme.ContentRemoveIcon(): + return "contentRemoveIcon" + case theme.ContentUndoIcon(): + return "contentUndoIcon" + case theme.DeleteIcon(): + return "deleteIcon" + case theme.DesktopIcon(): + return "desktopIcon" + case theme.DocumentCreateIcon(): + return "documentCreateIcon" + case theme.DocumentIcon(): + return "documentIcon" + case theme.DocumentPrintIcon(): + return "documentPrintIcon" + case theme.DocumentSaveIcon(): + return "documentSaveIcon" + case theme.DownloadIcon(): + return "downloadIcon" + case theme.ErrorIcon(): + return "errorIcon" + case theme.FileApplicationIcon(): + return "fileApplicationIcon" + case theme.FileAudioIcon(): + return "fileAudioIcon" + case theme.FileIcon(): + return "fileIcon" + case theme.FileImageIcon(): + return "fileImageIcon" + case theme.FileTextIcon(): + return "fileTextIcon" + case theme.FileVideoIcon(): + return "fileVideoIcon" + case theme.FolderIcon(): + return "folderIcon" + case theme.FolderNewIcon(): + return "folderNewIcon" + case theme.FolderOpenIcon(): + return "folderOpenIcon" + case theme.FyneLogo(): + return "fyneLogo" //lint:ignore SA1019 This needs to stay until the API is removed. + case theme.HelpIcon(): + return "helpIcon" + case theme.HistoryIcon(): + return "historyIcon" + case theme.HomeIcon(): + return "homeIcon" + case theme.InfoIcon(): + return "infoIcon" + case theme.MailAttachmentIcon(): + return "mailAttachementIcon" + case theme.MailComposeIcon(): + return "mailComposeIcon" + case theme.MailForwardIcon(): + return "mailForwardIcon" + case theme.MailReplyAllIcon(): + return "mailReplyAllIcon" + case theme.MailReplyIcon(): + return "mailReplyIcon" + case theme.MailSendIcon(): + return "mailSendIcon" + case theme.MediaFastForwardIcon(): + return "mediaFastForwardIcon" + case theme.MediaFastRewindIcon(): + return "mediaFastRewindIcon" + case theme.MediaPauseIcon(): + return "mediaPauseIcon" + case theme.MediaPlayIcon(): + return "mediaPlayIcon" + case theme.MediaRecordIcon(): + return "mediaRecordIcon" + case theme.MediaReplayIcon(): + return "mediaReplayIcon" + case theme.MediaSkipNextIcon(): + return "mediaSkipNextIcon" + case theme.MediaSkipPreviousIcon(): + return "mediaSkipPreviousIcon" + case theme.MenuDropDownIcon(): + return "menuDropDownIcon" + case theme.MenuDropUpIcon(): + return "menuDropUpIcon" + case theme.MenuExpandIcon(): + return "menuExpandIcon" + case theme.MenuIcon(): + return "menuIcon" + case theme.MoveDownIcon(): + return "moveDownIcon" + case theme.MoveUpIcon(): + return "moveUpIcon" + case theme.NavigateBackIcon(): + return "navigateBackIcon" + case theme.NavigateNextIcon(): + return "navigateNextIcon" + case theme.QuestionIcon(): + return "questionIcon" + case theme.RadioButtonCheckedIcon(): + return "radioButtonCheckedIcon" + case theme.RadioButtonFillIcon(): + return "radioButtonFillIcon" + case theme.RadioButtonIcon(): + return "radioButtonIcon" + case theme.SearchIcon(): + return "searchIcon" + case theme.SearchReplaceIcon(): + return "searchReplaceIcon" + case theme.SettingsIcon(): + return "settingsIcon" + case theme.StorageIcon(): + return "storageIcon" + case theme.ViewFullScreenIcon(): + return "viewFullScreenIcon" + case theme.ViewRefreshIcon(): + return "viewRefreshIcon" + case theme.ViewRestoreIcon(): + return "viewRestoreIcon" + case theme.VisibilityIcon(): + return "visibilityIcon" + case theme.VisibilityOffIcon(): + return "visibilityOffIcon" + case theme.VolumeDownIcon(): + return "volumeDownIcon" + case theme.VolumeMuteIcon(): + return "volumeMuteIcon" + case theme.VolumeUpIcon(): + return "volumeUpIcon" + case theme.WarningIcon(): + return "warningIcon" + case theme.ZoomFitIcon(): + return "zoomFitIcon" + case theme.ZoomInIcon(): + return "zoomInIcon" + case theme.ZoomOutIcon(): + return "zoomOutIcon" + default: + return "" + } +} + +func sortedKeys(m map[string]*string) []string { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} diff --git a/vendor/fyne.io/fyne/v2/test/notification.go b/vendor/fyne.io/fyne/v2/test/notification.go new file mode 100644 index 0000000..56e5404 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/test/notification.go @@ -0,0 +1 @@ +package test diff --git a/vendor/fyne.io/fyne/v2/test/notification_helper.go b/vendor/fyne.io/fyne/v2/test/notification_helper.go new file mode 100644 index 0000000..f73acd2 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/test/notification_helper.go @@ -0,0 +1,33 @@ +//go:build !tamago && !noos + +package test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "fyne.io/fyne/v2" +) + +// AssertNotificationSent allows an app developer to assert that a notification was sent. +// After the content of f has executed this utility will check that the specified notification was sent. +func AssertNotificationSent(t *testing.T, n *fyne.Notification, f func()) { + require.NotNil(t, f, "function has to be specified") + require.IsType(t, &app{}, fyne.CurrentApp()) + a := fyne.CurrentApp().(*app) + a.lastNotification = nil + + f() + if n == nil { + assert.Nil(t, a.lastNotification) + return + } else if a.lastNotification == nil { + t.Error("No notification sent") + return + } + + assert.Equal(t, n.Title, a.lastNotification.Title) + assert.Equal(t, n.Content, a.lastNotification.Content) +} diff --git a/vendor/fyne.io/fyne/v2/test/storage.go b/vendor/fyne.io/fyne/v2/test/storage.go new file mode 100644 index 0000000..c904710 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/test/storage.go @@ -0,0 +1,21 @@ +package test + +import ( + "os" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal" + "fyne.io/fyne/v2/storage" +) + +type testStorage struct { + *internal.Docs +} + +func (s *testStorage) RootURI() fyne.URI { + return storage.NewFileURI(os.TempDir()) +} + +func (s *testStorage) docRootURI() (fyne.URI, error) { + return storage.Child(s.RootURI(), "Documents") +} diff --git a/vendor/fyne.io/fyne/v2/test/test.go b/vendor/fyne.io/fyne/v2/test/test.go new file mode 100644 index 0000000..9658fa5 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/test/test.go @@ -0,0 +1,243 @@ +package test + +import ( + "os" + "path/filepath" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/driver/desktop" + "fyne.io/fyne/v2/internal/cache" + intdriver "fyne.io/fyne/v2/internal/driver" +) + +// RenderObjectToMarkup renders the given [fyne.io/fyne/v2.CanvasObject] to a markup string. +// +// Since: 2.6 +func RenderObjectToMarkup(o fyne.CanvasObject) string { + c := NewCanvas() + c.SetPadded(false) + size := o.MinSize().Max(o.Size()) + c.SetContent(o) + c.Resize(size) // ensure we are large enough for current size + + return snapshot(c) +} + +// RenderToMarkup renders the given [fyne.io/fyne/v2.Canvas] to a markup string. +// +// Since: 2.6 +func RenderToMarkup(c fyne.Canvas) string { + return snapshot(c) +} + +// Drag drags at an absolute position on the canvas. +// deltaX/Y is the dragging distance: <0 for dragging up/left, >0 for dragging down/right. +func Drag(c fyne.Canvas, pos fyne.Position, deltaX, deltaY float32) { + matches := func(object fyne.CanvasObject) bool { + _, ok := object.(fyne.Draggable) + return ok + } + o, p, _ := intdriver.FindObjectAtPositionMatching(pos, matches, c.Overlays().Top(), c.Content()) + if o == nil { + return + } + e := &fyne.DragEvent{ + PointEvent: fyne.PointEvent{Position: p}, + Dragged: fyne.Delta{DX: deltaX, DY: deltaY}, + } + o.(fyne.Draggable).Dragged(e) + o.(fyne.Draggable).DragEnd() +} + +// FocusNext focuses the next focusable on the canvas. +func FocusNext(c fyne.Canvas) { + if tc, ok := c.(*canvas); ok { + tc.focusManager().FocusNext() + } else { + fyne.LogError("FocusNext can only be called with a test canvas", nil) + } +} + +// FocusPrevious focuses the previous focusable on the canvas. +func FocusPrevious(c fyne.Canvas) { + if tc, ok := c.(*canvas); ok { + tc.focusManager().FocusPrevious() + } else { + fyne.LogError("FocusPrevious can only be called with a test canvas", nil) + } +} + +// LaidOutObjects returns all fyne.CanvasObject starting at the given fyne.CanvasObject which is laid out previously. +func LaidOutObjects(o fyne.CanvasObject) (objects []fyne.CanvasObject) { + if o != nil { + objects = layoutAndCollect(objects, o, o.MinSize().Max(o.Size())) + } + return objects +} + +// MoveMouse simulates a mouse movement to the given position. +func MoveMouse(c fyne.Canvas, pos fyne.Position) { + if fyne.CurrentDevice().IsMobile() { + return + } + + tc, _ := c.(*canvas) + var oldHovered, hovered desktop.Hoverable + if tc != nil { + oldHovered = tc.hovered + } + matches := func(object fyne.CanvasObject) bool { + _, ok := object.(desktop.Hoverable) + return ok + } + o, p, _ := intdriver.FindObjectAtPositionMatching(pos, matches, c.Overlays().Top(), c.Content()) + if o != nil { + hovered = o.(desktop.Hoverable) + me := &desktop.MouseEvent{ + PointEvent: fyne.PointEvent{ + AbsolutePosition: pos, + Position: p, + }, + } + if hovered == oldHovered { + hovered.MouseMoved(me) + } else { + if oldHovered != nil { + oldHovered.MouseOut() + } + hovered.MouseIn(me) + } + } else if oldHovered != nil { + oldHovered.MouseOut() + } + if tc != nil { + tc.hovered = hovered + } +} + +// Scroll scrolls at an absolute position on the canvas. +// deltaX/Y is the scrolling distance: <0 for scrolling up/left, >0 for scrolling down/right. +func Scroll(c fyne.Canvas, pos fyne.Position, deltaX, deltaY float32) { + matches := func(object fyne.CanvasObject) bool { + _, ok := object.(fyne.Scrollable) + return ok + } + o, _, _ := intdriver.FindObjectAtPositionMatching(pos, matches, c.Overlays().Top(), c.Content()) + if o == nil { + return + } + + e := &fyne.ScrollEvent{Scrolled: fyne.Delta{DX: deltaX, DY: deltaY}} + o.(fyne.Scrollable).Scrolled(e) +} + +// DoubleTap simulates a double left mouse click on the specified object. +func DoubleTap(obj fyne.DoubleTappable) { + ev, c := prepareTap(obj, fyne.NewPos(1, 1)) + handleFocusOnTap(c, obj) + obj.DoubleTapped(ev) +} + +// Tap simulates a left mouse click on the specified object. +func Tap(obj fyne.Tappable) { + TapAt(obj, fyne.NewPos(1, 1)) +} + +// TapAt simulates a left mouse click on the passed object at a specified place within it. +func TapAt(obj fyne.Tappable, pos fyne.Position) { + ev, c := prepareTap(obj, pos) + tap(c, obj, ev) +} + +// TapCanvas taps at an absolute position on the canvas. +func TapCanvas(c fyne.Canvas, pos fyne.Position) { + if o, p := findTappable(c, pos); o != nil { + tap(c, o.(fyne.Tappable), &fyne.PointEvent{AbsolutePosition: pos, Position: p}) + } +} + +// TapSecondary simulates a right mouse click on the specified object. +func TapSecondary(obj fyne.SecondaryTappable) { + TapSecondaryAt(obj, fyne.NewPos(1, 1)) +} + +// TapSecondaryAt simulates a right mouse click on the passed object at a specified place within it. +func TapSecondaryAt(obj fyne.SecondaryTappable, pos fyne.Position) { + ev, c := prepareTap(obj, pos) + handleFocusOnTap(c, obj) + obj.TappedSecondary(ev) +} + +// Type performs a series of key events to simulate typing of a value into the specified object. +// The focusable object will be focused before typing begins. +// The chars parameter will be input one rune at a time to the focused object. +func Type(obj fyne.Focusable, chars string) { + obj.FocusGained() + + typeChars([]rune(chars), obj.TypedRune) +} + +// TypeOnCanvas is like the Type function but it passes the key events to the canvas object +// rather than a focusable widget. +func TypeOnCanvas(c fyne.Canvas, chars string) { + typeChars([]rune(chars), c.OnTypedRune()) +} + +// WidgetRenderer allows test scripts to gain access to the current renderer for a widget. +// This can be used for verifying correctness of rendered components for a widget in unit tests. +func WidgetRenderer(wid fyne.Widget) fyne.WidgetRenderer { + return cache.Renderer(wid) +} + +func findTappable(c fyne.Canvas, pos fyne.Position) (o fyne.CanvasObject, p fyne.Position) { + matches := func(object fyne.CanvasObject) bool { + _, ok := object.(fyne.Tappable) + return ok + } + o, p, _ = intdriver.FindObjectAtPositionMatching(pos, matches, c.Overlays().Top(), c.Content()) + return o, p +} + +func prepareTap(obj any, pos fyne.Position) (*fyne.PointEvent, fyne.Canvas) { + d := fyne.CurrentApp().Driver() + ev := &fyne.PointEvent{Position: pos} + var c fyne.Canvas + if co, ok := obj.(fyne.CanvasObject); ok { + c = d.CanvasForObject(co) + ev.AbsolutePosition = d.AbsolutePositionForObject(co).Add(pos) + } + return ev, c +} + +func tap(c fyne.Canvas, obj fyne.Tappable, ev *fyne.PointEvent) { + handleFocusOnTap(c, obj) + obj.Tapped(ev) +} + +func handleFocusOnTap(c fyne.Canvas, obj any) { + if c == nil { + return + } + + if focus, ok := obj.(fyne.Focusable); ok { + dis, ok := obj.(fyne.Disableable) + if (!ok || !dis.Disabled()) && focus == c.Focused() { + return + } + } + + c.Unfocus() +} + +func typeChars(chars []rune, keyDown func(rune)) { + for _, char := range chars { + keyDown(char) + } +} + +func writeMarkup(path string, markup string) error { + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return err + } + return os.WriteFile(path, []byte(markup), 0o644) +} diff --git a/vendor/fyne.io/fyne/v2/test/test_helper.go b/vendor/fyne.io/fyne/v2/test/test_helper.go new file mode 100644 index 0000000..800b7d8 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/test/test_helper.go @@ -0,0 +1,160 @@ +//go:build !tamago && !noos + +package test + +import ( + "fmt" + "image" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/cache" + "fyne.io/fyne/v2/internal/painter/software" + "fyne.io/fyne/v2/internal/test" +) + +// AssertCanvasTappableAt asserts that the canvas is tappable at the given position. +func AssertCanvasTappableAt(t *testing.T, c fyne.Canvas, pos fyne.Position) bool { + if o, _ := findTappable(c, pos); o == nil { + t.Errorf("No tappable found at %#v", pos) + return false + } + return true +} + +// AssertObjectRendersToImage asserts that the given `CanvasObject` renders the same image as the one stored in the master file. +// The theme used is the standard test theme which may look different to how it shows on your device. +// The master filename is relative to the `testdata` directory which is relative to the test. +// The test `t` fails if the given image is not equal to the loaded master image. +// In this case the given image is written into a file in `testdata/failed/` (relative to the test). +// This path is also reported, thus the file can be used as new master. +// +// Since 2.3 +func AssertObjectRendersToImage(t *testing.T, masterFilename string, o fyne.CanvasObject, msgAndArgs ...any) bool { + c := NewCanvasWithPainter(software.NewPainter()) + c.SetPadded(false) + size := o.MinSize().Max(o.Size()) + c.SetContent(o) + c.Resize(size) // ensure we are large enough for current size + + return AssertRendersToImage(t, masterFilename, c, msgAndArgs...) +} + +// AssertObjectRendersToMarkup asserts that the given `CanvasObject` renders the same markup as the one stored in the master file. +// The master filename is relative to the `testdata` directory which is relative to the test. +// The test `t` fails if the rendered markup is not equal to the loaded master markup. +// In this case the rendered markup is written into a file in `testdata/failed/` (relative to the test). +// This path is also reported, thus the file can be used as new master. +// +// Be aware, that the indentation has to use tab characters ('\t') instead of spaces. +// Every element starts on a new line indented one more than its parent. +// Closing elements stand on their own line, too, using the same indentation as the opening element. +// The only exception to this are text elements which do not contain line breaks unless the text includes them. +// +// Since 2.3 +func AssertObjectRendersToMarkup(t *testing.T, masterFilename string, o fyne.CanvasObject, msgAndArgs ...any) bool { + c := NewCanvas() + c.SetPadded(false) + size := o.MinSize().Max(o.Size()) + c.SetContent(o) + c.Resize(size) // ensure we are large enough for current size + + return AssertRendersToMarkup(t, masterFilename, c, msgAndArgs...) +} + +// AssertImageMatches asserts that the given image is the same as the one stored in the master file. +// The master filename is relative to the `testdata` directory which is relative to the test. +// The test `t` fails if the given image is not equal to the loaded master image. +// In this case the given image is written into a file in `testdata/failed/` (relative to the test). +// This path is also reported, thus the file can be used as new master. +func AssertImageMatches(t *testing.T, masterFilename string, img image.Image, msgAndArgs ...any) bool { + return test.AssertImageMatches(t, masterFilename, img, msgAndArgs...) +} + +// AssertRendersToImage asserts that the given canvas renders the same image as the one stored in the master file. +// The master filename is relative to the `testdata` directory which is relative to the test. +// The test `t` fails if the given image is not equal to the loaded master image. +// In this case the given image is written into a file in `testdata/failed/` (relative to the test). +// This path is also reported, thus the file can be used as new master. +// +// Since 2.3 +func AssertRendersToImage(t *testing.T, masterFilename string, c fyne.Canvas, msgAndArgs ...any) bool { + return AssertImageMatches(t, masterFilename, c.Capture(), msgAndArgs...) +} + +// AssertRendersToMarkup asserts that the given canvas renders the same markup as the one stored in the master file. +// The master filename is relative to the `testdata` directory which is relative to the test. +// The test `t` fails if the rendered markup is not equal to the loaded master markup. +// In this case the rendered markup is written into a file in `testdata/failed/` (relative to the test). +// This path is also reported, thus the file can be used as new master. +// +// Be aware, that the indentation has to use tab characters ('\t') instead of spaces. +// Every element starts on a new line indented one more than its parent. +// Closing elements stand on their own line, too, using the same indentation as the opening element. +// The only exception to this are text elements which do not contain line breaks unless the text includes them. +// +// Since: 2.0 +func AssertRendersToMarkup(t *testing.T, masterFilename string, c fyne.Canvas, msgAndArgs ...any) bool { + wd, err := os.Getwd() + require.NoError(t, err) + + got := snapshot(c) + masterPath := filepath.Join(wd, "testdata", masterFilename) + failedPath := filepath.Join(wd, "testdata/failed", masterFilename) + _, err = os.Stat(masterPath) + if os.IsNotExist(err) { + require.NoError(t, writeMarkup(failedPath, got)) + t.Errorf("Master not found at %s. Markup written to %s might be used as master.", masterPath, failedPath) + return false + } + + raw, err := os.ReadFile(masterPath) + require.NoError(t, err) + master := strings.ReplaceAll(string(raw), "\r", "") + + var msg string + if len(msgAndArgs) > 0 { + msg = fmt.Sprintf(msgAndArgs[0].(string)+"\n", msgAndArgs[1:]...) + } + if !assert.Equal(t, master, got, "%sMarkup did not match master. Actual markup written to file://%s.", msg, failedPath) { + require.NoError(t, writeMarkup(failedPath, got)) + return false + } + return true +} + +// ApplyTheme sets the given theme and waits for it to be applied to the current app. +func ApplyTheme(t *testing.T, theme fyne.Theme) { + require.IsType(t, &app{}, fyne.CurrentApp()) + a := fyne.CurrentApp().(*app) + a.Settings().SetTheme(theme) + for a.lastAppliedTheme() != theme { + time.Sleep(5 * time.Millisecond) + } +} + +// TempWidgetRenderer allows test scripts to gain access to the current renderer for a widget. +// This can be used for verifying correctness of rendered components for a widget in unit tests. +// The widget renderer is automatically destroyed when the test ends. +// +// Since: 2.5 +func TempWidgetRenderer(t *testing.T, wid fyne.Widget) fyne.WidgetRenderer { + t.Cleanup(func() { cache.DestroyRenderer(wid) }) + return cache.Renderer(wid) +} + +// WithTestTheme runs a function with the testTheme temporarily set. +func WithTestTheme(t *testing.T, f func()) { + settings := fyne.CurrentApp().Settings() + current := settings.Theme() + ApplyTheme(t, NewTheme()) + defer ApplyTheme(t, current) + f() +} diff --git a/vendor/fyne.io/fyne/v2/test/theme.go b/vendor/fyne.io/fyne/v2/test/theme.go new file mode 100644 index 0000000..80b3fb2 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/test/theme.go @@ -0,0 +1,234 @@ +package test + +import ( + "fmt" + "image/color" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/theme" +) + +var defaultTheme fyne.Theme + +// Try to keep these in sync with the existing color names at theme/color.go. +var knownColorNames = [...]fyne.ThemeColorName{ + theme.ColorNameBackground, + theme.ColorNameButton, + theme.ColorNameDisabled, + theme.ColorNameDisabledButton, + theme.ColorNameError, + theme.ColorNameFocus, + theme.ColorNameForeground, + theme.ColorNameForegroundOnError, + theme.ColorNameForegroundOnPrimary, + theme.ColorNameForegroundOnSuccess, + theme.ColorNameForegroundOnWarning, + theme.ColorNameHeaderBackground, + theme.ColorNameHover, + theme.ColorNameHyperlink, + theme.ColorNameInputBackground, + theme.ColorNameInputBorder, + theme.ColorNameMenuBackground, + theme.ColorNameOverlayBackground, + theme.ColorNamePlaceHolder, + theme.ColorNamePressed, + theme.ColorNamePrimary, + theme.ColorNameScrollBar, + theme.ColorNameScrollBarBackground, + theme.ColorNameSelection, + theme.ColorNameSeparator, + theme.ColorNameShadow, + theme.ColorNameSuccess, + theme.ColorNameWarning, +} + +// KnownThemeVariants returns the known theme variants mapped by a descriptive key. +func KnownThemeVariants() map[string]fyne.ThemeVariant { + // Try to keep this in sync with the existing variants at theme/theme.go + return map[string]fyne.ThemeVariant{ + "dark": theme.VariantDark, + "light": theme.VariantLight, + } +} + +// NewTheme returns a new test theme using quiet ugly colors. +func NewTheme() fyne.Theme { + blue := func(alpha uint8) color.Color { + return &color.NRGBA{R: 0, G: 0, B: 255, A: alpha} + } + gray := func(level uint8) color.Color { + return &color.Gray{Y: level} + } + green := func(alpha uint8) color.Color { + return &color.NRGBA{R: 0, G: 255, B: 0, A: alpha} + } + red := func(alpha uint8) color.Color { + return &color.NRGBA{R: 200, G: 0, B: 0, A: alpha} + } + + return &configurableTheme{ + colors: map[fyne.ThemeColorName]color.Color{ + theme.ColorNameBackground: red(255), + theme.ColorNameButton: gray(100), + theme.ColorNameDisabled: gray(20), + theme.ColorNameDisabledButton: gray(230), + theme.ColorNameError: blue(255), + theme.ColorNameFocus: red(66), + theme.ColorNameForeground: gray(255), + theme.ColorNameForegroundOnError: red(210), + theme.ColorNameForegroundOnPrimary: red(200), + theme.ColorNameForegroundOnSuccess: blue(201), + theme.ColorNameForegroundOnWarning: blue(202), + theme.ColorNameHeaderBackground: red(22), + theme.ColorNameHover: green(200), + theme.ColorNameHyperlink: blue(240), + theme.ColorNameInputBackground: red(30), + theme.ColorNameInputBorder: gray(10), + theme.ColorNameMenuBackground: red(50), + theme.ColorNameOverlayBackground: red(44), + theme.ColorNamePlaceHolder: blue(200), + theme.ColorNamePressed: blue(250), + theme.ColorNamePrimary: green(255), + theme.ColorNameScrollBar: blue(220), + theme.ColorNameScrollBarBackground: red(20), + theme.ColorNameSelection: red(55), + theme.ColorNameSeparator: gray(30), + theme.ColorNameShadow: blue(150), + theme.ColorNameSuccess: green(150), + theme.ColorNameWarning: red(100), + }, + fonts: map[fyne.TextStyle]fyne.Resource{ + {}: theme.DefaultTextBoldFont(), + {Bold: true}: theme.DefaultTextItalicFont(), + {Bold: true, Italic: true}: theme.DefaultTextMonospaceFont(), + {Italic: true}: theme.DefaultTextBoldItalicFont(), + {Monospace: true}: theme.DefaultTextFont(), + {Symbol: true}: theme.DefaultSymbolFont(), + }, + name: "Ugly Test Theme", + sizes: map[fyne.ThemeSizeName]float32{ + theme.SizeNameInlineIcon: float32(24), + theme.SizeNameInnerPadding: float32(20), + theme.SizeNameLineSpacing: float32(6), + theme.SizeNamePadding: float32(10), + theme.SizeNameScrollBar: float32(10), + theme.SizeNameScrollBarSmall: float32(2), + theme.SizeNameSeparatorThickness: float32(1), + theme.SizeNameText: float32(18), + theme.SizeNameHeadingText: float32(30.6), + theme.SizeNameSubHeadingText: float32(24), + theme.SizeNameCaptionText: float32(15), + theme.SizeNameInputBorder: float32(5), + theme.SizeNameInputRadius: float32(2), + theme.SizeNameSelectionRadius: float32(6), + theme.SizeNameScrollBarRadius: float32(2), + }, + } +} + +// Theme returns a test theme useful for image based tests. +func Theme() fyne.Theme { + if defaultTheme == nil { + defaultTheme = &configurableTheme{ + colors: map[fyne.ThemeColorName]color.Color{ + theme.ColorNameBackground: color.NRGBA{R: 0x44, G: 0x44, B: 0x44, A: 0xff}, + theme.ColorNameButton: color.NRGBA{R: 0x33, G: 0x33, B: 0x33, A: 0xff}, + theme.ColorNameDisabled: color.NRGBA{R: 0x88, G: 0x88, B: 0x88, A: 0xff}, + theme.ColorNameDisabledButton: color.NRGBA{R: 0x22, G: 0x22, B: 0x22, A: 0xff}, + theme.ColorNameError: color.NRGBA{R: 0xf4, G: 0x43, B: 0x36, A: 0xff}, + theme.ColorNameFocus: color.NRGBA{R: 0x78, G: 0x3a, B: 0x3a, A: 0xff}, + theme.ColorNameForeground: color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}, + theme.ColorNameForegroundOnError: color.NRGBA{R: 0x08, G: 0x0a, B: 0x0f, A: 0xff}, + theme.ColorNameForegroundOnPrimary: color.NRGBA{R: 0x08, G: 0x0c, B: 0x0f, A: 0xff}, + theme.ColorNameForegroundOnSuccess: color.NRGBA{R: 0x0a, G: 0x0c, B: 0x0f, A: 0xff}, + theme.ColorNameForegroundOnWarning: color.NRGBA{R: 0x08, G: 0x0c, B: 0x0a, A: 0xff}, + theme.ColorNameHeaderBackground: color.NRGBA{R: 0x25, G: 0x25, B: 0x25, A: 0xff}, + theme.ColorNameHover: color.NRGBA{R: 0x88, G: 0xff, B: 0xff, A: 0x22}, + theme.ColorNameHyperlink: color.NRGBA{R: 0xff, G: 0xcc, B: 0x80, A: 0xff}, + theme.ColorNameInputBackground: color.NRGBA{R: 0x66, G: 0x66, B: 0x66, A: 0xff}, + theme.ColorNameInputBorder: color.NRGBA{R: 0x86, G: 0x86, B: 0x86, A: 0xff}, + theme.ColorNameMenuBackground: color.NRGBA{R: 0x56, G: 0x56, B: 0x56, A: 0xff}, + theme.ColorNameOverlayBackground: color.NRGBA{R: 0x28, G: 0x28, B: 0x28, A: 0xff}, + theme.ColorNamePlaceHolder: color.NRGBA{R: 0xaa, G: 0xaa, B: 0xaa, A: 0xff}, + theme.ColorNamePressed: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0x33}, + theme.ColorNamePrimary: color.NRGBA{R: 0xff, G: 0xc0, B: 0x80, A: 0xff}, + theme.ColorNameScrollBar: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xaa}, + theme.ColorNameScrollBarBackground: color.NRGBA{R: 0x67, G: 0x66, B: 0x66, A: 0xff}, + theme.ColorNameSelection: color.NRGBA{R: 0x78, G: 0x3a, B: 0x3a, A: 0x99}, + theme.ColorNameSeparator: color.NRGBA{R: 0x90, G: 0x90, B: 0x90, A: 0xff}, + theme.ColorNameShadow: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0x88}, + theme.ColorNameSuccess: color.NRGBA{R: 0x00, G: 0x99, B: 0x00, A: 0xff}, + theme.ColorNameWarning: color.NRGBA{R: 0xee, G: 0xee, B: 0x00, A: 0xff}, + }, + fonts: map[fyne.TextStyle]fyne.Resource{ + {}: theme.DefaultTextFont(), + {Bold: true}: theme.DefaultTextBoldFont(), + {Bold: true, Italic: true}: theme.DefaultTextBoldItalicFont(), + {Italic: true}: theme.DefaultTextItalicFont(), + {Monospace: true}: theme.DefaultTextMonospaceFont(), + {Symbol: true}: theme.DefaultSymbolFont(), + }, + name: "Default Test Theme", + sizes: map[fyne.ThemeSizeName]float32{ + theme.SizeNameInlineIcon: float32(20), + theme.SizeNameInnerPadding: float32(8), + theme.SizeNameLineSpacing: float32(4), + theme.SizeNamePadding: float32(4), + theme.SizeNameScrollBar: float32(16), + theme.SizeNameScrollBarSmall: float32(3), + theme.SizeNameSeparatorThickness: float32(1), + theme.SizeNameText: float32(14), + theme.SizeNameHeadingText: float32(23.8), + theme.SizeNameSubHeadingText: float32(18), + theme.SizeNameCaptionText: float32(11), + theme.SizeNameInputBorder: float32(2), + theme.SizeNameInputRadius: float32(4), + theme.SizeNameSelectionRadius: float32(4), + theme.SizeNameScrollBarRadius: float32(3), + theme.SizeNameWindowTitleBarHeight: float32(20), + theme.SizeNameWindowButtonHeight: float32(10), + theme.SizeNameWindowButtonIcon: float32(8), + theme.SizeNameWindowButtonRadius: float32(5), + }, + } + } + return defaultTheme +} + +type configurableTheme struct { + colors map[fyne.ThemeColorName]color.Color + fonts map[fyne.TextStyle]fyne.Resource + name string + sizes map[fyne.ThemeSizeName]float32 +} + +var _ fyne.Theme = (*configurableTheme)(nil) + +func (t *configurableTheme) Color(n fyne.ThemeColorName, _ fyne.ThemeVariant) color.Color { + if t.colors[n] == nil { + fyne.LogError(fmt.Sprintf("color %s not defined in theme %s", n, t.name), nil) + } + + return t.colors[n] +} + +func (t *configurableTheme) Font(style fyne.TextStyle) fyne.Resource { + if t.fonts[style] == nil { + fyne.LogError(fmt.Sprintf("font for style %#v not defined in theme %s", style, t.name), nil) + } + + return t.fonts[style] +} + +func (t *configurableTheme) Icon(n fyne.ThemeIconName) fyne.Resource { + return theme.DefaultTheme().Icon(n) +} + +func (t *configurableTheme) Size(s fyne.ThemeSizeName) float32 { + if _, ok := t.sizes[s]; !ok { + fyne.LogError(fmt.Sprintf("size %s not defined in theme %s", s, t.name), nil) + return 0 + } + + return t.sizes[s] +} diff --git a/vendor/fyne.io/fyne/v2/test/theme_helper.go b/vendor/fyne.io/fyne/v2/test/theme_helper.go new file mode 100644 index 0000000..4e7e5f9 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/test/theme_helper.go @@ -0,0 +1,31 @@ +//go:build !tamago && !noos + +package test + +import ( + "image/color" + "testing" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/theme" + "github.com/stretchr/testify/assert" +) + +// AssertAllColorNamesDefined asserts that all known color names are defined for the given theme. +func AssertAllColorNamesDefined(t *testing.T, th fyne.Theme, themeName string) { + oldApp := fyne.CurrentApp() + defer fyne.SetCurrentApp(oldApp) + + for _, primaryName := range theme.PrimaryColorNames() { + testApp := NewTempApp(t) + testApp.Settings().(*testSettings).primaryColor = primaryName + for variantName, variant := range KnownThemeVariants() { + for _, cn := range knownColorNames { + assert.NotNil(t, th.Color(cn, variant), "undefined color %s variant %s in theme %s", cn, variantName, themeName) + // Transparent is used by the default theme as fallback for unknown color names. + // Built-in color names should have well-defined non-transparent values. + assert.NotEqual(t, color.Transparent, th.Color(cn, variant), "undefined color %s variant %s in theme %s", cn, variantName, themeName) + } + } + } +} diff --git a/vendor/fyne.io/fyne/v2/test/window.go b/vendor/fyne.io/fyne/v2/test/window.go new file mode 100644 index 0000000..1036ef0 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/test/window.go @@ -0,0 +1,140 @@ +package test + +import ( + "fyne.io/fyne/v2" +) + +type window struct { + title string + fullScreen bool + fixedSize bool + focused bool + onClosed func() + onCloseIntercepted func() + + canvas *canvas + driver *driver + menu *fyne.MainMenu +} + +// NewWindow creates and registers a new window for test purposes +func NewWindow(content fyne.CanvasObject) fyne.Window { + window := fyne.CurrentApp().NewWindow("") + window.SetContent(content) + return window +} + +func (w *window) Canvas() fyne.Canvas { + return w.canvas +} + +func (w *window) CenterOnScreen() { + // no-op +} + +func (w *window) Clipboard() fyne.Clipboard { + return NewClipboard() +} + +func (w *window) Close() { + if w.onClosed != nil { + w.onClosed() + } + w.focused = false + w.driver.removeWindow(w) +} + +func (w *window) Content() fyne.CanvasObject { + return w.Canvas().Content() +} + +func (w *window) FixedSize() bool { + return w.fixedSize +} + +func (w *window) FullScreen() bool { + return w.fullScreen +} + +func (w *window) Hide() { + w.focused = false +} + +func (w *window) Icon() fyne.Resource { + return fyne.CurrentApp().Icon() +} + +func (w *window) MainMenu() *fyne.MainMenu { + return w.menu +} + +func (w *window) Padded() bool { + return w.canvas.Padded() +} + +func (w *window) RequestFocus() { + for _, win := range w.driver.AllWindows() { + win.(*window).focused = false + } + + w.focused = true +} + +func (w *window) Resize(size fyne.Size) { + w.canvas.Resize(size) +} + +func (w *window) SetContent(obj fyne.CanvasObject) { + w.Canvas().SetContent(obj) +} + +func (w *window) SetFixedSize(fixed bool) { + w.fixedSize = fixed +} + +func (w *window) SetIcon(_ fyne.Resource) { + // no-op +} + +func (w *window) SetFullScreen(fullScreen bool) { + w.fullScreen = fullScreen +} + +func (w *window) SetMainMenu(menu *fyne.MainMenu) { + w.menu = menu +} + +func (w *window) SetMaster() { + // no-op +} + +func (w *window) SetOnClosed(closed func()) { + w.onClosed = closed +} + +func (w *window) SetCloseIntercept(callback func()) { + w.onCloseIntercepted = callback +} + +func (w *window) SetOnDropped(dropped func(fyne.Position, []fyne.URI)) { +} + +func (w *window) SetPadded(padded bool) { + w.canvas.SetPadded(padded) +} + +func (w *window) SetTitle(title string) { + w.title = title +} + +func (w *window) Show() { + w.RequestFocus() +} + +func (w *window) ShowAndRun() { + w.Show() +} + +func (w *window) Title() string { + return w.title +} diff --git a/vendor/fyne.io/fyne/v2/test/window_helper.go b/vendor/fyne.io/fyne/v2/test/window_helper.go new file mode 100644 index 0000000..235ff39 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/test/window_helper.go @@ -0,0 +1,19 @@ +//go:build !tamago && !noos + +package test + +import ( + "testing" + + "fyne.io/fyne/v2" +) + +// NewTempWindow creates and registers a new window for test purposes. +// This window will get removed automatically once the running test ends. +// +// Since: 2.5 +func NewTempWindow(t testing.TB, content fyne.CanvasObject) fyne.Window { + window := NewWindow(content) + t.Cleanup(window.Close) + return window +} diff --git a/vendor/fyne.io/fyne/v2/text.go b/vendor/fyne.io/fyne/v2/text.go new file mode 100644 index 0000000..a300811 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/text.go @@ -0,0 +1,74 @@ +package fyne + +// TextAlign represents the horizontal alignment of text within a widget or +// canvas object. +type TextAlign int + +const ( + // TextAlignLeading specifies a left alignment for left-to-right languages. + TextAlignLeading TextAlign = iota + // TextAlignCenter places the text centrally within the available space. + TextAlignCenter + // TextAlignTrailing will align the text right for a left-to-right language. + TextAlignTrailing +) + +// TextTruncation controls how a `Label` or `Entry` will truncate its text. +// The default value is `TextTruncateOff` which will not truncate. +// +// Since: 2.4 +type TextTruncation int + +const ( + // TextTruncateOff means no truncation will be applied, it is the default. + // This means that the minimum size of a text block will be the space required to display it fully. + TextTruncateOff TextTruncation = iota + // TextTruncateClip will truncate text when it reaches the end of the available space. + TextTruncateClip + // TextTruncateEllipsis is like regular truncation except that an ellipses (…) will be inserted + // wherever text has been shortened to fit. + // + // Since: 2.4 + TextTruncateEllipsis +) + +// TextWrap represents how text longer than the widget's width will be wrapped. +type TextWrap int + +const ( + // TextWrapOff extends the widget's width to fit the text, no wrapping is applied. + TextWrapOff TextWrap = iota + // TextTruncate trims the text to the widget's width, no wrapping is applied. + // If an entry is asked to truncate it will provide scrolling capabilities. + // + // Deprecated: Use [TextTruncateClip] value of the widget `Truncation` field instead + TextTruncate + // TextWrapBreak trims the line of characters to the widget's width adding the excess as new line. + // An Entry with text wrapping will scroll vertically if there is not enough space for all the text. + TextWrapBreak + // TextWrapWord trims the line of words to the widget's width adding the excess as new line. + // An Entry with text wrapping will scroll vertically if there is not enough space for all the text. + TextWrapWord +) + +// TextStyle represents the styles that can be applied to a text canvas object +// or text based widget. +type TextStyle struct { + Bold bool // Should text be bold + Italic bool // Should text be italic + Monospace bool // Use the system monospace font instead of regular + // Since: 2.2 + Symbol bool // Use the system symbol font. + // Since: 2.1 + TabWidth int // Width of tabs in spaces + // Since: 2.5 + // Currently only supported by [fyne.io/fyne/v2/widget.TextGrid]. + Underline bool // Should text be underlined. +} + +// MeasureText uses the current driver to calculate the size of text when rendered. +// The font used will be read from the current app's theme. +func MeasureText(text string, size float32, style TextStyle) Size { + s, _ := CurrentApp().Driver().RenderedTextSize(text, size, style, nil) + return s +} diff --git a/vendor/fyne.io/fyne/v2/theme.go b/vendor/fyne.io/fyne/v2/theme.go new file mode 100644 index 0000000..5d02233 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme.go @@ -0,0 +1,63 @@ +package fyne + +import "image/color" + +// ThemeVariant indicates a variation of a theme, such as light or dark. +// +// Since: 2.0 +type ThemeVariant uint + +// ThemeColorName is used to look up a colour based on its name. +// +// Since: 2.0 +type ThemeColorName string + +// ThemeIconName is used to look up an icon based on its name. +// +// Since: 2.0 +type ThemeIconName string + +// ThemeSizeName is used to look up a size based on its name. +// +// Since: 2.0 +type ThemeSizeName string + +// Theme defines the method to look up colors, sizes and fonts that make up a Fyne theme. +// +// Since: 2.0 +type Theme interface { + Color(ThemeColorName, ThemeVariant) color.Color + Font(TextStyle) Resource + Icon(ThemeIconName) Resource + Size(ThemeSizeName) float32 +} + +// LegacyTheme defines the requirements of any Fyne theme. +// This was previously called Theme and is kept for simpler transition of applications built before v2.0.0. +// +// Since: 2.0 +type LegacyTheme interface { + BackgroundColor() color.Color + ButtonColor() color.Color + DisabledButtonColor() color.Color + TextColor() color.Color + DisabledTextColor() color.Color + PlaceHolderColor() color.Color + PrimaryColor() color.Color + HoverColor() color.Color + FocusColor() color.Color + ScrollBarColor() color.Color + ShadowColor() color.Color + + TextSize() int + TextFont() Resource + TextBoldFont() Resource + TextItalicFont() Resource + TextBoldItalicFont() Resource + TextMonospaceFont() Resource + + Padding() int + IconInlineSize() int + ScrollBarSize() int + ScrollBarSmallSize() int +} diff --git a/vendor/fyne.io/fyne/v2/theme/bundled-emoji.go b/vendor/fyne.io/fyne/v2/theme/bundled-emoji.go new file mode 100644 index 0000000..14d4b0e --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/bundled-emoji.go @@ -0,0 +1,17 @@ +//go:build !no_emoji + +package theme + +import ( + _ "embed" + + "fyne.io/fyne/v2" +) + +//go:embed font/EmojiOneColor.otf +var emojiFontData []byte + +var emoji = &fyne.StaticResource{ + StaticName: "EmojiOneColor.otf", + StaticContent: emojiFontData, +} diff --git a/vendor/fyne.io/fyne/v2/theme/bundled-fonts.go b/vendor/fyne.io/fyne/v2/theme/bundled-fonts.go new file mode 100644 index 0000000..f48103b --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/bundled-fonts.go @@ -0,0 +1,55 @@ +package theme + +import ( + _ "embed" + + "fyne.io/fyne/v2" +) + +//go:embed font/NotoSans-Regular.ttf +var notoSansRegular []byte + +var regular = &fyne.StaticResource{ + StaticName: "NotoSans-Regular.ttf", + StaticContent: notoSansRegular, +} + +//go:embed font/NotoSans-Bold.ttf +var notoSansBold []byte + +var bold = &fyne.StaticResource{ + StaticName: "NotoSans-Bold.ttf", + StaticContent: notoSansBold, +} + +//go:embed font/NotoSans-Italic.ttf +var notoSansItalic []byte + +var italic = &fyne.StaticResource{ + StaticName: "NotoSans-Italic.ttf", + StaticContent: notoSansItalic, +} + +//go:embed font/NotoSans-BoldItalic.ttf +var notoSansBoldItalic []byte + +var bolditalic = &fyne.StaticResource{ + StaticName: "NotoSans-BoldItalic.ttf", + StaticContent: notoSansBoldItalic, +} + +//go:embed font/DejaVuSansMono-Powerline.ttf +var dejaVuSansMono []byte + +var monospace = &fyne.StaticResource{ + StaticName: "DejaVuSansMono-Powerline.ttf", + StaticContent: dejaVuSansMono, +} + +//go:embed font/InterSymbols-Regular.ttf +var interSymbolsRegular []byte + +var symbol = &fyne.StaticResource{ + StaticName: "InterSymbols-Regular.ttf", + StaticContent: interSymbolsRegular, +} diff --git a/vendor/fyne.io/fyne/v2/theme/bundled-icons.go b/vendor/fyne.io/fyne/v2/theme/bundled-icons.go new file mode 100644 index 0000000..4389dad --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/bundled-icons.go @@ -0,0 +1,783 @@ +package theme + +import ( + _ "embed" + + "fyne.io/fyne/v2" +) + +//go:embed icons/fyne.png +var fyneLogo []byte + +var fynelogo = &fyne.StaticResource{ + StaticName: "fyne.png", + StaticContent: fyneLogo, +} + +//go:embed icons/cancel.svg +var cancelIcon []byte + +var cancelIconRes = &fyne.StaticResource{ + StaticName: "cancel.svg", + StaticContent: cancelIcon, +} + +//go:embed icons/check.svg +var checkIcon []byte + +var checkIconRes = &fyne.StaticResource{ + StaticName: "check.svg", + StaticContent: checkIcon, +} + +//go:embed icons/delete.svg +var deleteIcon []byte + +var deleteIconRes = &fyne.StaticResource{ + StaticName: "delete.svg", + StaticContent: deleteIcon, +} + +//go:embed icons/search.svg +var searchIcon []byte + +var searchIconRes = &fyne.StaticResource{ + StaticName: "search.svg", + StaticContent: searchIcon, +} + +//go:embed icons/search-replace.svg +var searchreplaceIcon []byte + +var searchreplaceIconRes = &fyne.StaticResource{ + StaticName: "search-replace.svg", + StaticContent: searchreplaceIcon, +} + +//go:embed icons/menu.svg +var menuIcon []byte + +var menuIconRes = &fyne.StaticResource{ + StaticName: "menu.svg", + StaticContent: menuIcon, +} + +//go:embed icons/menu-expand.svg +var menuexpandIcon []byte + +var menuexpandIconRes = &fyne.StaticResource{ + StaticName: "menu-expand.svg", + StaticContent: menuexpandIcon, +} + +//go:embed icons/check-box.svg +var checkboxIcon []byte + +var checkboxIconRes = &fyne.StaticResource{ + StaticName: "check-box.svg", + StaticContent: checkboxIcon, +} + +//go:embed icons/check-box-checked.svg +var checkboxcheckedIcon []byte + +var checkboxcheckedIconRes = &fyne.StaticResource{ + StaticName: "check-box-checked.svg", + StaticContent: checkboxcheckedIcon, +} + +//go:embed icons/check-box-fill.svg +var checkboxfillIcon []byte + +var checkboxfillIconRes = &fyne.StaticResource{ + StaticName: "check-box-fill.svg", + StaticContent: checkboxfillIcon, +} + +//go:embed icons/check-box-partial.svg +var checkboxpartialIcon []byte + +var checkboxpartialIconRes = &fyne.StaticResource{ + StaticName: "check-box-partial.svg", + StaticContent: checkboxpartialIcon, +} + +//go:embed icons/radio-button.svg +var radiobuttonIcon []byte + +var radiobuttonIconRes = &fyne.StaticResource{ + StaticName: "radio-button.svg", + StaticContent: radiobuttonIcon, +} + +//go:embed icons/radio-button-checked.svg +var radiobuttoncheckedIcon []byte + +var radiobuttoncheckedIconRes = &fyne.StaticResource{ + StaticName: "radio-button-checked.svg", + StaticContent: radiobuttoncheckedIcon, +} + +//go:embed icons/radio-button-fill.svg +var radiobuttonfillIcon []byte + +var radiobuttonfillIconRes = &fyne.StaticResource{ + StaticName: "radio-button-fill.svg", + StaticContent: radiobuttonfillIcon, +} + +//go:embed icons/content-add.svg +var contentaddIcon []byte + +var contentaddIconRes = &fyne.StaticResource{ + StaticName: "content-add.svg", + StaticContent: contentaddIcon, +} + +//go:embed icons/content-remove.svg +var contentremoveIcon []byte + +var contentremoveIconRes = &fyne.StaticResource{ + StaticName: "content-remove.svg", + StaticContent: contentremoveIcon, +} + +//go:embed icons/content-cut.svg +var contentcutIcon []byte + +var contentcutIconRes = &fyne.StaticResource{ + StaticName: "content-cut.svg", + StaticContent: contentcutIcon, +} + +//go:embed icons/content-copy.svg +var contentcopyIcon []byte + +var contentcopyIconRes = &fyne.StaticResource{ + StaticName: "content-copy.svg", + StaticContent: contentcopyIcon, +} + +//go:embed icons/content-paste.svg +var contentpasteIcon []byte + +var contentpasteIconRes = &fyne.StaticResource{ + StaticName: "content-paste.svg", + StaticContent: contentpasteIcon, +} + +//go:embed icons/content-redo.svg +var contentredoIcon []byte + +var contentredoIconRes = &fyne.StaticResource{ + StaticName: "content-redo.svg", + StaticContent: contentredoIcon, +} + +//go:embed icons/content-undo.svg +var contentundoIcon []byte + +var contentundoIconRes = &fyne.StaticResource{ + StaticName: "content-undo.svg", + StaticContent: contentundoIcon, +} + +//go:embed icons/color-achromatic.svg +var colorachromaticIcon []byte + +var colorachromaticIconRes = &fyne.StaticResource{ + StaticName: "color-achromatic.svg", + StaticContent: colorachromaticIcon, +} + +//go:embed icons/color-chromatic.svg +var colorchromaticIcon []byte + +var colorchromaticIconRes = &fyne.StaticResource{ + StaticName: "color-chromatic.svg", + StaticContent: colorchromaticIcon, +} + +//go:embed icons/color-palette.svg +var colorpaletteIcon []byte + +var colorpaletteIconRes = &fyne.StaticResource{ + StaticName: "color-palette.svg", + StaticContent: colorpaletteIcon, +} + +//go:embed icons/document.svg +var documentIcon []byte + +var documentIconRes = &fyne.StaticResource{ + StaticName: "document.svg", + StaticContent: documentIcon, +} + +//go:embed icons/document-create.svg +var documentcreateIcon []byte + +var documentcreateIconRes = &fyne.StaticResource{ + StaticName: "document-create.svg", + StaticContent: documentcreateIcon, +} + +//go:embed icons/document-print.svg +var documentprintIcon []byte + +var documentprintIconRes = &fyne.StaticResource{ + StaticName: "document-print.svg", + StaticContent: documentprintIcon, +} + +//go:embed icons/document-save.svg +var documentsaveIcon []byte + +var documentsaveIconRes = &fyne.StaticResource{ + StaticName: "document-save.svg", + StaticContent: documentsaveIcon, +} + +//go:embed icons/drag-corner-indicator.svg +var dragcornerindicatorIcon []byte + +var dragcornerindicatorIconRes = &fyne.StaticResource{ + StaticName: "drag-corner-indicator.svg", + StaticContent: dragcornerindicatorIcon, +} + +//go:embed icons/more-horizontal.svg +var morehorizontalIcon []byte + +var morehorizontalIconRes = &fyne.StaticResource{ + StaticName: "more-horizontal.svg", + StaticContent: morehorizontalIcon, +} + +//go:embed icons/more-vertical.svg +var moreverticalIcon []byte + +var moreverticalIconRes = &fyne.StaticResource{ + StaticName: "more-vertical.svg", + StaticContent: moreverticalIcon, +} + +//go:embed icons/info.svg +var infoIcon []byte + +var infoIconRes = &fyne.StaticResource{ + StaticName: "info.svg", + StaticContent: infoIcon, +} + +//go:embed icons/question.svg +var questionIcon []byte + +var questionIconRes = &fyne.StaticResource{ + StaticName: "question.svg", + StaticContent: questionIcon, +} + +//go:embed icons/warning.svg +var warningIcon []byte + +var warningIconRes = &fyne.StaticResource{ + StaticName: "warning.svg", + StaticContent: warningIcon, +} + +//go:embed icons/error.svg +var errorIcon []byte + +var errorIconRes = &fyne.StaticResource{ + StaticName: "error.svg", + StaticContent: errorIcon, +} + +//go:embed icons/broken-image.svg +var brokenimageIcon []byte + +var brokenimageIconRes = &fyne.StaticResource{ + StaticName: "broken-image.svg", + StaticContent: brokenimageIcon, +} + +//go:embed icons/arrow-back.svg +var arrowbackIcon []byte + +var arrowbackIconRes = &fyne.StaticResource{ + StaticName: "arrow-back.svg", + StaticContent: arrowbackIcon, +} + +//go:embed icons/arrow-down.svg +var arrowdownIcon []byte + +var arrowdownIconRes = &fyne.StaticResource{ + StaticName: "arrow-down.svg", + StaticContent: arrowdownIcon, +} + +//go:embed icons/arrow-forward.svg +var arrowforwardIcon []byte + +var arrowforwardIconRes = &fyne.StaticResource{ + StaticName: "arrow-forward.svg", + StaticContent: arrowforwardIcon, +} + +//go:embed icons/arrow-up.svg +var arrowupIcon []byte + +var arrowupIconRes = &fyne.StaticResource{ + StaticName: "arrow-up.svg", + StaticContent: arrowupIcon, +} + +//go:embed icons/arrow-drop-down.svg +var arrowdropdownIcon []byte + +var arrowdropdownIconRes = &fyne.StaticResource{ + StaticName: "arrow-drop-down.svg", + StaticContent: arrowdropdownIcon, +} + +//go:embed icons/arrow-drop-up.svg +var arrowdropupIcon []byte + +var arrowdropupIconRes = &fyne.StaticResource{ + StaticName: "arrow-drop-up.svg", + StaticContent: arrowdropupIcon, +} + +//go:embed icons/file.svg +var fileIcon []byte + +var fileIconRes = &fyne.StaticResource{ + StaticName: "file.svg", + StaticContent: fileIcon, +} + +//go:embed icons/file-application.svg +var fileapplicationIcon []byte + +var fileapplicationIconRes = &fyne.StaticResource{ + StaticName: "file-application.svg", + StaticContent: fileapplicationIcon, +} + +//go:embed icons/file-audio.svg +var fileaudioIcon []byte + +var fileaudioIconRes = &fyne.StaticResource{ + StaticName: "file-audio.svg", + StaticContent: fileaudioIcon, +} + +//go:embed icons/file-image.svg +var fileimageIcon []byte + +var fileimageIconRes = &fyne.StaticResource{ + StaticName: "file-image.svg", + StaticContent: fileimageIcon, +} + +//go:embed icons/file-text.svg +var filetextIcon []byte + +var filetextIconRes = &fyne.StaticResource{ + StaticName: "file-text.svg", + StaticContent: filetextIcon, +} + +//go:embed icons/file-video.svg +var filevideoIcon []byte + +var filevideoIconRes = &fyne.StaticResource{ + StaticName: "file-video.svg", + StaticContent: filevideoIcon, +} + +//go:embed icons/folder.svg +var folderIcon []byte + +var folderIconRes = &fyne.StaticResource{ + StaticName: "folder.svg", + StaticContent: folderIcon, +} + +//go:embed icons/folder-new.svg +var foldernewIcon []byte + +var foldernewIconRes = &fyne.StaticResource{ + StaticName: "folder-new.svg", + StaticContent: foldernewIcon, +} + +//go:embed icons/folder-open.svg +var folderopenIcon []byte + +var folderopenIconRes = &fyne.StaticResource{ + StaticName: "folder-open.svg", + StaticContent: folderopenIcon, +} + +//go:embed icons/help.svg +var helpIcon []byte + +var helpIconRes = &fyne.StaticResource{ + StaticName: "help.svg", + StaticContent: helpIcon, +} + +//go:embed icons/history.svg +var historyIcon []byte + +var historyIconRes = &fyne.StaticResource{ + StaticName: "history.svg", + StaticContent: historyIcon, +} + +//go:embed icons/home.svg +var homeIcon []byte + +var homeIconRes = &fyne.StaticResource{ + StaticName: "home.svg", + StaticContent: homeIcon, +} + +//go:embed icons/settings.svg +var settingsIcon []byte + +var settingsIconRes = &fyne.StaticResource{ + StaticName: "settings.svg", + StaticContent: settingsIcon, +} + +//go:embed icons/mail-attachment.svg +var mailattachmentIcon []byte + +var mailattachmentIconRes = &fyne.StaticResource{ + StaticName: "mail-attachment.svg", + StaticContent: mailattachmentIcon, +} + +//go:embed icons/mail-compose.svg +var mailcomposeIcon []byte + +var mailcomposeIconRes = &fyne.StaticResource{ + StaticName: "mail-compose.svg", + StaticContent: mailcomposeIcon, +} + +//go:embed icons/mail-forward.svg +var mailforwardIcon []byte + +var mailforwardIconRes = &fyne.StaticResource{ + StaticName: "mail-forward.svg", + StaticContent: mailforwardIcon, +} + +//go:embed icons/mail-reply.svg +var mailreplyIcon []byte + +var mailreplyIconRes = &fyne.StaticResource{ + StaticName: "mail-reply.svg", + StaticContent: mailreplyIcon, +} + +//go:embed icons/mail-reply_all.svg +var mailreplyallIcon []byte + +var mailreplyallIconRes = &fyne.StaticResource{ + StaticName: "mail-reply_all.svg", + StaticContent: mailreplyallIcon, +} + +//go:embed icons/mail-send.svg +var mailsendIcon []byte + +var mailsendIconRes = &fyne.StaticResource{ + StaticName: "mail-send.svg", + StaticContent: mailsendIcon, +} + +//go:embed icons/media-music.svg +var mediamusicIcon []byte + +var mediamusicIconRes = &fyne.StaticResource{ + StaticName: "media-music.svg", + StaticContent: mediamusicIcon, +} + +//go:embed icons/media-photo.svg +var mediaphotoIcon []byte + +var mediaphotoIconRes = &fyne.StaticResource{ + StaticName: "media-photo.svg", + StaticContent: mediaphotoIcon, +} + +//go:embed icons/media-video.svg +var mediavideoIcon []byte + +var mediavideoIconRes = &fyne.StaticResource{ + StaticName: "media-video.svg", + StaticContent: mediavideoIcon, +} + +//go:embed icons/media-fast-forward.svg +var mediafastforwardIcon []byte + +var mediafastforwardIconRes = &fyne.StaticResource{ + StaticName: "media-fast-forward.svg", + StaticContent: mediafastforwardIcon, +} + +//go:embed icons/media-fast-rewind.svg +var mediafastrewindIcon []byte + +var mediafastrewindIconRes = &fyne.StaticResource{ + StaticName: "media-fast-rewind.svg", + StaticContent: mediafastrewindIcon, +} + +//go:embed icons/media-pause.svg +var mediapauseIcon []byte + +var mediapauseIconRes = &fyne.StaticResource{ + StaticName: "media-pause.svg", + StaticContent: mediapauseIcon, +} + +//go:embed icons/media-play.svg +var mediaplayIcon []byte + +var mediaplayIconRes = &fyne.StaticResource{ + StaticName: "media-play.svg", + StaticContent: mediaplayIcon, +} + +//go:embed icons/media-record.svg +var mediarecordIcon []byte + +var mediarecordIconRes = &fyne.StaticResource{ + StaticName: "media-record.svg", + StaticContent: mediarecordIcon, +} + +//go:embed icons/media-replay.svg +var mediareplayIcon []byte + +var mediareplayIconRes = &fyne.StaticResource{ + StaticName: "media-replay.svg", + StaticContent: mediareplayIcon, +} + +//go:embed icons/media-skip-next.svg +var mediaskipnextIcon []byte + +var mediaskipnextIconRes = &fyne.StaticResource{ + StaticName: "media-skip-next.svg", + StaticContent: mediaskipnextIcon, +} + +//go:embed icons/media-skip-previous.svg +var mediaskippreviousIcon []byte + +var mediaskippreviousIconRes = &fyne.StaticResource{ + StaticName: "media-skip-previous.svg", + StaticContent: mediaskippreviousIcon, +} + +//go:embed icons/media-stop.svg +var mediastopIcon []byte + +var mediastopIconRes = &fyne.StaticResource{ + StaticName: "media-stop.svg", + StaticContent: mediastopIcon, +} + +//go:embed icons/view-fullscreen.svg +var viewfullscreenIcon []byte + +var viewfullscreenIconRes = &fyne.StaticResource{ + StaticName: "view-fullscreen.svg", + StaticContent: viewfullscreenIcon, +} + +//go:embed icons/view-refresh.svg +var viewrefreshIcon []byte + +var viewrefreshIconRes = &fyne.StaticResource{ + StaticName: "view-refresh.svg", + StaticContent: viewrefreshIcon, +} + +//go:embed icons/view-zoom-fit.svg +var viewzoomfitIcon []byte + +var viewzoomfitIconRes = &fyne.StaticResource{ + StaticName: "view-zoom-fit.svg", + StaticContent: viewzoomfitIcon, +} + +//go:embed icons/view-zoom-in.svg +var viewzoominIcon []byte + +var viewzoominIconRes = &fyne.StaticResource{ + StaticName: "view-zoom-in.svg", + StaticContent: viewzoominIcon, +} + +//go:embed icons/view-zoom-out.svg +var viewzoomoutIcon []byte + +var viewzoomoutIconRes = &fyne.StaticResource{ + StaticName: "view-zoom-out.svg", + StaticContent: viewzoomoutIcon, +} + +//go:embed icons/volume-down.svg +var volumedownIcon []byte + +var volumedownIconRes = &fyne.StaticResource{ + StaticName: "volume-down.svg", + StaticContent: volumedownIcon, +} + +//go:embed icons/volume-mute.svg +var volumemuteIcon []byte + +var volumemuteIconRes = &fyne.StaticResource{ + StaticName: "volume-mute.svg", + StaticContent: volumemuteIcon, +} + +//go:embed icons/volume-up.svg +var volumeupIcon []byte + +var volumeupIconRes = &fyne.StaticResource{ + StaticName: "volume-up.svg", + StaticContent: volumeupIcon, +} + +//go:embed icons/visibility.svg +var visibilityIcon []byte + +var visibilityIconRes = &fyne.StaticResource{ + StaticName: "visibility.svg", + StaticContent: visibilityIcon, +} + +//go:embed icons/visibility-off.svg +var visibilityoffIcon []byte + +var visibilityoffIconRes = &fyne.StaticResource{ + StaticName: "visibility-off.svg", + StaticContent: visibilityoffIcon, +} + +//go:embed icons/download.svg +var downloadIcon []byte + +var downloadIconRes = &fyne.StaticResource{ + StaticName: "download.svg", + StaticContent: downloadIcon, +} + +//go:embed icons/computer.svg +var computerIcon []byte + +var computerIconRes = &fyne.StaticResource{ + StaticName: "computer.svg", + StaticContent: computerIcon, +} + +//go:embed icons/desktop.svg +var desktopIcon []byte + +var desktopIconRes = &fyne.StaticResource{ + StaticName: "desktop.svg", + StaticContent: desktopIcon, +} + +//go:embed icons/storage.svg +var storageIcon []byte + +var storageIconRes = &fyne.StaticResource{ + StaticName: "storage.svg", + StaticContent: storageIcon, +} + +//go:embed icons/upload.svg +var uploadIcon []byte + +var uploadIconRes = &fyne.StaticResource{ + StaticName: "upload.svg", + StaticContent: uploadIcon, +} + +//go:embed icons/account.svg +var accountIcon []byte + +var accountIconRes = &fyne.StaticResource{ + StaticName: "account.svg", + StaticContent: accountIcon, +} + +//go:embed icons/calendar.svg +var calendarIcon []byte + +var calendarIconRes = &fyne.StaticResource{ + StaticName: "calendar.svg", + StaticContent: calendarIcon, +} + +//go:embed icons/login.svg +var loginIcon []byte + +var loginIconRes = &fyne.StaticResource{ + StaticName: "login.svg", + StaticContent: loginIcon, +} + +//go:embed icons/logout.svg +var logoutIcon []byte + +var logoutIconRes = &fyne.StaticResource{ + StaticName: "logout.svg", + StaticContent: logoutIcon, +} + +//go:embed icons/list.svg +var listIcon []byte + +var listIconRes = &fyne.StaticResource{ + StaticName: "list.svg", + StaticContent: listIcon, +} + +//go:embed icons/grid.svg +var gridIcon []byte + +var gridIconRes = &fyne.StaticResource{ + StaticName: "grid.svg", + StaticContent: gridIcon, +} + +//go:embed icons/maximize.svg +var maximizeIcon []byte + +var maximizeIconRes = &fyne.StaticResource{ + StaticName: "maximize.svg", + StaticContent: maximizeIcon, +} + +//go:embed icons/minimize.svg +var minimizeIcon []byte + +var minimizeIconRes = &fyne.StaticResource{ + StaticName: "minimize.svg", + StaticContent: minimizeIcon, +} diff --git a/vendor/fyne.io/fyne/v2/theme/color.go b/vendor/fyne.io/fyne/v2/theme/color.go new file mode 100644 index 0000000..8aac84b --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/color.go @@ -0,0 +1,482 @@ +package theme + +import ( + "image/color" + + "fyne.io/fyne/v2" + internaltheme "fyne.io/fyne/v2/internal/theme" +) + +// Keep in mind to add new constants to the tests at test/theme.go. +const ( + // ColorRed is the red primary color name. + // + // Since: 1.4 + ColorRed = internaltheme.ColorRed + // ColorOrange is the orange primary color name. + // + // Since: 1.4 + ColorOrange = internaltheme.ColorOrange + // ColorYellow is the yellow primary color name. + // + // Since: 1.4 + ColorYellow = internaltheme.ColorYellow + // ColorGreen is the green primary color name. + // + // Since: 1.4 + ColorGreen = internaltheme.ColorGreen + // ColorBlue is the blue primary color name. + // + // Since: 1.4 + ColorBlue = internaltheme.ColorBlue + // ColorPurple is the purple primary color name. + // + // Since: 1.4 + ColorPurple = internaltheme.ColorPurple + // ColorBrown is the brown primary color name. + // + // Since: 1.4 + ColorBrown = internaltheme.ColorBrown + // ColorGray is the gray primary color name. + // + // Since: 1.4 + ColorGray = internaltheme.ColorGray + + // ColorNameBackground is the name of theme lookup for background color. + // + // Since: 2.0 + ColorNameBackground fyne.ThemeColorName = "background" + + // ColorNameButton is the name of theme lookup for button color. + // + // Since: 2.0 + ColorNameButton fyne.ThemeColorName = "button" + + // ColorNameDisabledButton is the name of theme lookup for disabled button color. + // + // Since: 2.0 + ColorNameDisabledButton fyne.ThemeColorName = "disabledButton" + + // ColorNameDisabled is the name of theme lookup for disabled foreground color. + // + // Since: 2.0 + ColorNameDisabled fyne.ThemeColorName = "disabled" + + // ColorNameError is the name of theme lookup for error color. + // + // Since: 2.0 + ColorNameError fyne.ThemeColorName = "error" + + // ColorNameFocus is the name of theme lookup for focus color. + // + // Since: 2.0 + ColorNameFocus fyne.ThemeColorName = "focus" + + // ColorNameForeground is the name of theme lookup for foreground color. + // + // Since: 2.0 + ColorNameForeground fyne.ThemeColorName = "foreground" + + // ColorNameForegroundOnError is the name of theme lookup for a contrast color to the error color. + // + // Since: 2.5 + ColorNameForegroundOnError fyne.ThemeColorName = "foregroundOnError" + + // ColorNameForegroundOnPrimary is the name of theme lookup for a contrast color to the primary color. + // + // Since: 2.5 + ColorNameForegroundOnPrimary fyne.ThemeColorName = "foregroundOnPrimary" + + // ColorNameForegroundOnSuccess is the name of theme lookup for a contrast color to the success color. + // + // Since: 2.5 + ColorNameForegroundOnSuccess fyne.ThemeColorName = "foregroundOnSuccess" + + // ColorNameForegroundOnWarning is the name of theme lookup for a contrast color to the warning color. + // + // Since: 2.5 + ColorNameForegroundOnWarning fyne.ThemeColorName = "foregroundOnWarning" + + // ColorNameHeaderBackground is the name of theme lookup for background color of a collection header. + // + // Since: 2.4 + ColorNameHeaderBackground fyne.ThemeColorName = "headerBackground" + + // ColorNameHover is the name of theme lookup for hover color. + // + // Since: 2.0 + ColorNameHover fyne.ThemeColorName = "hover" + + // ColorNameHyperlink is the name of theme lookup for hyperlink color. + // + // Since: 2.4 + ColorNameHyperlink fyne.ThemeColorName = "hyperlink" + + // ColorNameInputBackground is the name of theme lookup for background color of an input field. + // + // Since: 2.0 + ColorNameInputBackground fyne.ThemeColorName = "inputBackground" + + // ColorNameInputBorder is the name of theme lookup for border color of an input field. + // + // Since: 2.3 + ColorNameInputBorder fyne.ThemeColorName = "inputBorder" + + // ColorNameMenuBackground is the name of theme lookup for background color of menus. + // + // Since: 2.3 + ColorNameMenuBackground fyne.ThemeColorName = "menuBackground" + + // ColorNameOverlayBackground is the name of theme lookup for background color of overlays like dialogs. + // + // Since: 2.3 + ColorNameOverlayBackground fyne.ThemeColorName = "overlayBackground" + + // ColorNamePlaceHolder is the name of theme lookup for placeholder text color. + // + // Since: 2.0 + ColorNamePlaceHolder fyne.ThemeColorName = "placeholder" + + // ColorNamePressed is the name of theme lookup for the tap overlay color. + // + // Since: 2.0 + ColorNamePressed fyne.ThemeColorName = "pressed" + + // ColorNamePrimary is the name of theme lookup for primary color. + // + // Since: 2.0 + ColorNamePrimary fyne.ThemeColorName = "primary" + + // ColorNameScrollBar is the name of theme lookup for scrollbar color. + // + // Since: 2.0 + ColorNameScrollBar fyne.ThemeColorName = "scrollBar" + + // ColorNameScrollBarBackground is the name of theme lookup for scrollbar background color. + // + // Since: 2.6 + ColorNameScrollBarBackground fyne.ThemeColorName = "scrollBarBackground" + + // ColorNameSelection is the name of theme lookup for selection color. + // + // Since: 2.1 + ColorNameSelection fyne.ThemeColorName = "selection" + + // ColorNameSeparator is the name of theme lookup for separator bars. + // + // Since: 2.3 + ColorNameSeparator fyne.ThemeColorName = "separator" + + // ColorNameShadow is the name of theme lookup for shadow color. + // + // Since: 2.0 + ColorNameShadow fyne.ThemeColorName = "shadow" + + // ColorNameSuccess is the name of theme lookup for success color. + // + // Since: 2.3 + ColorNameSuccess fyne.ThemeColorName = "success" + + // ColorNameWarning is the name of theme lookup for warning color. + // + // Since: 2.3 + ColorNameWarning fyne.ThemeColorName = "warning" +) + +var ( + colorDarkBackground = color.NRGBA{R: 0x17, G: 0x17, B: 0x18, A: 0xff} + colorDarkButton = color.NRGBA{R: 0x28, G: 0x29, B: 0x2e, A: 0xff} + colorDarkDisabled = color.NRGBA{R: 0x39, G: 0x39, B: 0x3a, A: 0xff} + colorDarkDisabledButton = color.NRGBA{R: 0x28, G: 0x29, B: 0x2e, A: 0xff} + colorDarkError = color.NRGBA{R: 0xf4, G: 0x43, B: 0x36, A: 0xff} + colorDarkForeground = color.NRGBA{R: 0xf3, G: 0xf3, B: 0xf3, A: 0xff} + colorDarkForegroundOnError = color.NRGBA{R: 0x17, G: 0x17, B: 0x18, A: 0xff} + colorDarkForegroundOnSuccess = color.NRGBA{R: 0x17, G: 0x17, B: 0x18, A: 0xff} + colorDarkForegroundOnWarning = color.NRGBA{R: 0x17, G: 0x17, B: 0x18, A: 0xff} + colorDarkHeaderBackground = color.NRGBA{R: 0x1b, G: 0x1b, B: 0x1b, A: 0xff} + colorDarkHover = color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0x0f} + colorDarkInputBackground = color.NRGBA{R: 0x20, G: 0x20, B: 0x23, A: 0xff} + colorDarkInputBorder = color.NRGBA{R: 0x39, G: 0x39, B: 0x3a, A: 0xff} + colorDarkMenuBackground = color.NRGBA{R: 0x28, G: 0x29, B: 0x2e, A: 0xff} + colorDarkOverlayBackground = color.NRGBA{R: 0x18, G: 0x1d, B: 0x25, A: 0xff} + colorDarkPlaceholder = color.NRGBA{R: 0xb2, G: 0xb2, B: 0xb2, A: 0xff} + colorDarkPressed = color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0x66} + colorDarkScrollBar = color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0x99} + colorDarkScrollBarBackground = color.NRGBA{R: 0x20, G: 0x20, B: 0x23, A: 0xff} + colorDarkSeparator = color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xff} + colorDarkShadow = color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0x66} + colorDarkSuccess = color.NRGBA{R: 0x43, G: 0xf4, B: 0x36, A: 0xff} + colorDarkWarning = color.NRGBA{R: 0xff, G: 0x98, B: 0x00, A: 0xff} + + colorLightBackground = color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff} + colorLightButton = color.NRGBA{R: 0xf5, G: 0xf5, B: 0xf5, A: 0xff} + colorLightDisabled = color.NRGBA{R: 0xe3, G: 0xe3, B: 0xe3, A: 0xff} + colorLightDisabledButton = color.NRGBA{R: 0xf5, G: 0xf5, B: 0xf5, A: 0xff} + colorLightError = color.NRGBA{R: 0xf4, G: 0x43, B: 0x36, A: 0xff} + colorLightFocusBlue = color.NRGBA{R: 0x00, G: 0x6c, B: 0xff, A: 0x2a} + colorLightFocusBrown = color.NRGBA{R: 0x79, G: 0x55, B: 0x48, A: 0x7f} + colorLightFocusGray = color.NRGBA{R: 0x9e, G: 0x9e, B: 0x9e, A: 0x7f} + colorLightFocusGreen = color.NRGBA{R: 0x8b, G: 0xc3, B: 0x4a, A: 0x7f} + colorLightFocusOrange = color.NRGBA{R: 0xff, G: 0x98, B: 0x00, A: 0x7f} + colorLightFocusPurple = color.NRGBA{R: 0x9c, G: 0x27, B: 0xb0, A: 0x7f} + colorLightFocusRed = color.NRGBA{R: 0xf4, G: 0x43, B: 0x36, A: 0x7f} + colorLightFocusYellow = color.NRGBA{R: 0xff, G: 0xeb, B: 0x3b, A: 0x7f} + colorLightForeground = color.NRGBA{R: 0x56, G: 0x56, B: 0x56, A: 0xff} + colorLightForegroundOnError = color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff} + colorLightForegroundOnSuccess = color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff} + colorLightForegroundOnWarning = color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff} + colorLightHeaderBackground = color.NRGBA{R: 0xf9, G: 0xf9, B: 0xf9, A: 0xff} + colorLightHover = color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0x0f} + colorLightInputBackground = color.NRGBA{R: 0xf3, G: 0xf3, B: 0xf3, A: 0xff} + colorLightInputBorder = color.NRGBA{R: 0xe3, G: 0xe3, B: 0xe3, A: 0xff} + colorLightMenuBackground = color.NRGBA{R: 0xf5, G: 0xf5, B: 0xf5, A: 0xff} + colorLightOverlayBackground = color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff} + colorLightPlaceholder = color.NRGBA{R: 0x88, G: 0x88, B: 0x88, A: 0xff} + colorLightPressed = color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0x19} + colorLightScrollBar = color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0x99} + colorLightScrollBarBackground = color.NRGBA{R: 0xdb, G: 0xdb, B: 0xdb, A: 0xff} + colorLightSelectionBlue = color.NRGBA{R: 0x00, G: 0x6c, B: 0xff, A: 0x40} + colorLightSelectionBrown = color.NRGBA{R: 0x79, G: 0x55, B: 0x48, A: 0x3f} + colorLightSelectionGray = color.NRGBA{R: 0x9e, G: 0x9e, B: 0x9e, A: 0x3f} + colorLightSelectionGreen = color.NRGBA{R: 0x8b, G: 0xc3, B: 0x4a, A: 0x3f} + colorLightSelectionOrange = color.NRGBA{R: 0xff, G: 0x98, B: 0x00, A: 0x3f} + colorLightSelectionPurple = color.NRGBA{R: 0x9c, G: 0x27, B: 0xb0, A: 0x3f} + colorLightSelectionRed = color.NRGBA{R: 0xf4, G: 0x43, B: 0x36, A: 0x3f} + colorLightSelectionYellow = color.NRGBA{R: 0xff, G: 0xeb, B: 0x3b, A: 0x3f} + colorLightSeparator = color.NRGBA{R: 0xe3, G: 0xe3, B: 0xe3, A: 0xff} + colorLightShadow = color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0x33} + colorLightSuccess = color.NRGBA{R: 0x43, G: 0xf4, B: 0x36, A: 0xff} + colorLightWarning = color.NRGBA{R: 0xff, G: 0x98, B: 0x00, A: 0xff} +) + +// BackgroundColor returns the theme's background color. +// +// Deprecated: Use Color(theme.ColorNameBackground) instead. +func BackgroundColor() color.Color { + return safeColorLookup(ColorNameBackground, currentVariant()) +} + +// ButtonColor returns the theme's standard button color. +// +// Deprecated: Use Color(theme.ColorNameButton) instead. +func ButtonColor() color.Color { + return safeColorLookup(ColorNameButton, currentVariant()) +} + +// Color looks up the named colour for current theme and variant. +// +// Since: 2.5 +func Color(name fyne.ThemeColorName) color.Color { + return safeColorLookup(name, currentVariant()) +} + +// ColorForWidget looks up the named colour for the requested widget using the current theme and variant. +// If the widget theme has been overridden that theme will be used. +// +// Since: 2.5 +func ColorForWidget(name fyne.ThemeColorName, w fyne.Widget) color.Color { + return CurrentForWidget(w).Color(name, currentVariant()) +} + +// DisabledButtonColor returns the theme's disabled button color. +// +// Deprecated: Use Color(theme.ColorNameDisabledButton) instead. +func DisabledButtonColor() color.Color { + return safeColorLookup(ColorNameDisabledButton, currentVariant()) +} + +// DisabledColor returns the foreground color for a disabled UI element. +// +// Since: 2.0 +// +// Deprecated: Use Color(theme.ColorNameDisabled) instead. +func DisabledColor() color.Color { + return safeColorLookup(ColorNameDisabled, currentVariant()) +} + +// DisabledTextColor returns the theme's disabled text color - this is actually the disabled color since 1.4. +// +// Deprecated: Use Color(theme.ColorNameDisabled) instead. +func DisabledTextColor() color.Color { + return DisabledColor() +} + +// ErrorColor returns the theme's error foreground color. +// +// Since: 2.0 +// +// Deprecated: Use Color(theme.ColorNameError) instead. +func ErrorColor() color.Color { + return safeColorLookup(ColorNameError, currentVariant()) +} + +// FocusColor returns the color used to highlight a focused widget. +// +// Deprecated: Use Color(theme.ColorNameFocus) instead. +func FocusColor() color.Color { + return safeColorLookup(ColorNameFocus, currentVariant()) +} + +// ForegroundColor returns the theme's standard foreground color for text and icons. +// +// Since: 2.0 +// +// Deprecated: Use Color(theme.ColorNameForeground) instead. +func ForegroundColor() color.Color { + return safeColorLookup(ColorNameForeground, currentVariant()) +} + +// HeaderBackgroundColor returns the color used to draw underneath collection headers. +// +// Since: 2.4 +// +// Deprecated: Use Color(theme.ColorNameHeaderBackground) instead. +func HeaderBackgroundColor() color.Color { + return Current().Color(ColorNameHeaderBackground, currentVariant()) +} + +// HoverColor returns the color used to highlight interactive elements currently under a cursor. +// +// Deprecated: Use Color(theme.ColorNameHover) instead. +func HoverColor() color.Color { + return safeColorLookup(ColorNameHover, currentVariant()) +} + +// HyperlinkColor returns the color used for the Hyperlink widget and hyperlink text elements. +// +// Deprecated: Use Color(theme.ColorNameHyperlink) instead. +func HyperlinkColor() color.Color { + return safeColorLookup(ColorNameHyperlink, currentVariant()) +} + +// InputBackgroundColor returns the color used to draw underneath input elements. +// +// Deprecated: Use Color(theme.ColorNameInputBackground) instead. +func InputBackgroundColor() color.Color { + return Current().Color(ColorNameInputBackground, currentVariant()) +} + +// InputBorderColor returns the color used to draw underneath input elements. +// +// Since: 2.3 +// +// Deprecated: Use Color(theme.ColorNameInputBorder) instead. +func InputBorderColor() color.Color { + return Current().Color(ColorNameInputBorder, currentVariant()) +} + +// MenuBackgroundColor returns the theme's background color for menus. +// +// Since: 2.3 +// +// Deprecated: Use Color(theme.ColorNameMenuBackground) instead. +func MenuBackgroundColor() color.Color { + return safeColorLookup(ColorNameMenuBackground, currentVariant()) +} + +// OverlayBackgroundColor returns the theme's background color for overlays like dialogs. +// +// Since: 2.3 +// +// Deprecated: Use Color(theme.ColorNameOverlayBackground) instead. +func OverlayBackgroundColor() color.Color { + return safeColorLookup(ColorNameOverlayBackground, currentVariant()) +} + +// PlaceHolderColor returns the theme's standard text color. +// +// Deprecated: Use Color(theme.ColorNamePlaceHolder) instead. +func PlaceHolderColor() color.Color { + return safeColorLookup(ColorNamePlaceHolder, currentVariant()) +} + +// PressedColor returns the color used to overlap tapped features. +// +// Since: 2.0 +// +// Deprecated: Use Color(theme.ColorNamePressed) instead. +func PressedColor() color.Color { + return safeColorLookup(ColorNamePressed, currentVariant()) +} + +// PrimaryColor returns the color used to highlight primary features. +// +// Deprecated: Use Color(theme.ColorNamePrimary) instead. +func PrimaryColor() color.Color { + return safeColorLookup(ColorNamePrimary, currentVariant()) +} + +// PrimaryColorNamed returns a theme specific color value for a named primary color. +// +// Since: 1.4 +// +// Deprecated: You should not access named primary colors but access the primary color using Color(theme.ColorNamePrimary) instead. +func PrimaryColorNamed(name string) color.Color { + return internaltheme.PrimaryColorNamed(name) +} + +// PrimaryColorNames returns a list of the standard primary color options. +// +// Since: 1.4 +func PrimaryColorNames() []string { + return []string{ColorRed, ColorOrange, ColorYellow, ColorGreen, ColorBlue, ColorPurple, ColorBrown, ColorGray} +} + +// ScrollBarColor returns the color (and translucency) for a scrollBar. +// +// Deprecated: Use Color(theme.ColorNameScrollBar) instead. +func ScrollBarColor() color.Color { + return safeColorLookup(ColorNameScrollBar, currentVariant()) +} + +// SelectionColor returns the color for a selected element. +// +// Since: 2.1 +// +// Deprecated: Use Color(theme.ColorNameSelection) instead. +func SelectionColor() color.Color { + return safeColorLookup(ColorNameSelection, currentVariant()) +} + +// SeparatorColor returns the color for the separator element. +// +// Since: 2.3 +// +// Deprecated: Use Color(theme.ColorNameSeparator) instead. +func SeparatorColor() color.Color { + return safeColorLookup(ColorNameSeparator, currentVariant()) +} + +// ShadowColor returns the color (and translucency) for shadows used for indicating elevation. +// +// Deprecated: Use Color(theme.ColorNameShadow) instead. +func ShadowColor() color.Color { + return safeColorLookup(ColorNameShadow, currentVariant()) +} + +// SuccessColor returns the theme's success foreground color. +// +// Since: 2.3 +// +// Deprecated: Use Color(theme.ColorNameSuccess) instead. +func SuccessColor() color.Color { + return safeColorLookup(ColorNameSuccess, currentVariant()) +} + +// WarningColor returns the theme's warning foreground color. +// +// Since: 2.3 +// +// Deprecated: Use Color(theme.ColorNameWarning) instead. +func WarningColor() color.Color { + return safeColorLookup(ColorNameWarning, currentVariant()) +} + +func safeColorLookup(n fyne.ThemeColorName, v fyne.ThemeVariant) color.Color { + col := Current().Color(n, v) + if col == nil { + fyne.LogError("Loaded theme returned nil color", nil) + return fallbackColor + } + return col +} diff --git a/vendor/fyne.io/fyne/v2/theme/font.go b/vendor/fyne.io/fyne/v2/theme/font.go new file mode 100644 index 0000000..2ad102d --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/font.go @@ -0,0 +1,119 @@ +package theme + +import ( + "image/color" + + "fyne.io/fyne/v2" +) + +// DefaultEmojiFont returns the font resource for the built-in emoji font. +// This may return nil if the application was packaged without an emoji font. +// +// Since: 2.4 +func DefaultEmojiFont() fyne.Resource { + return emoji +} + +// DefaultTextBoldFont returns the font resource for the built-in bold font style. +func DefaultTextBoldFont() fyne.Resource { + return bold +} + +// DefaultTextBoldItalicFont returns the font resource for the built-in bold and italic font style. +func DefaultTextBoldItalicFont() fyne.Resource { + return bolditalic +} + +// DefaultTextFont returns the font resource for the built-in regular font style. +func DefaultTextFont() fyne.Resource { + return regular +} + +// DefaultTextItalicFont returns the font resource for the built-in italic font style. +func DefaultTextItalicFont() fyne.Resource { + return italic +} + +// DefaultTextMonospaceFont returns the font resource for the built-in monospace font face. +func DefaultTextMonospaceFont() fyne.Resource { + return monospace +} + +// DefaultSymbolFont returns the font resource for the built-in symbol font. +// +// Since: 2.2 +func DefaultSymbolFont() fyne.Resource { + return symbol +} + +// Font looks up the font for current theme and text style. +// +// Since: 2.5 +func Font(style fyne.TextStyle) fyne.Resource { + return safeFontLookup(style) +} + +// TextBoldFont returns the font resource for the bold font style. +func TextBoldFont() fyne.Resource { + return safeFontLookup(fyne.TextStyle{Bold: true}) +} + +// TextBoldItalicFont returns the font resource for the bold and italic font style. +func TextBoldItalicFont() fyne.Resource { + return safeFontLookup(fyne.TextStyle{Bold: true, Italic: true}) +} + +// TextColor returns the theme's standard text color - this is actually the foreground color since 1.4. +// +// Deprecated: Use theme.ForegroundColor() colour instead. +func TextColor() color.Color { + return safeColorLookup(ColorNameForeground, currentVariant()) +} + +// TextFont returns the font resource for the regular font style. +func TextFont() fyne.Resource { + return safeFontLookup(fyne.TextStyle{}) +} + +// TextItalicFont returns the font resource for the italic font style. +func TextItalicFont() fyne.Resource { + return safeFontLookup(fyne.TextStyle{Italic: true}) +} + +// TextMonospaceFont returns the font resource for the monospace font face. +func TextMonospaceFont() fyne.Resource { + return safeFontLookup(fyne.TextStyle{Monospace: true}) +} + +// SymbolFont returns the font resource for the symbol font style. +// +// Since: 2.4 +func SymbolFont() fyne.Resource { + return safeFontLookup(fyne.TextStyle{Symbol: true}) +} + +func safeFontLookup(s fyne.TextStyle) fyne.Resource { + font := Current().Font(s) + if font != nil { + return font + } + fyne.LogError("Loaded theme returned nil font", nil) + + if s.Monospace { + return DefaultTextMonospaceFont() + } + if s.Bold { + if s.Italic { + return DefaultTextBoldItalicFont() + } + return DefaultTextBoldFont() + } + if s.Italic { + return DefaultTextItalicFont() + } + if s.Symbol { + return DefaultSymbolFont() + } + + return DefaultTextFont() +} diff --git a/vendor/fyne.io/fyne/v2/theme/font/DejaVuSansMono-Powerline.ttf b/vendor/fyne.io/fyne/v2/theme/font/DejaVuSansMono-Powerline.ttf new file mode 100644 index 0000000..3a6261a Binary files /dev/null and b/vendor/fyne.io/fyne/v2/theme/font/DejaVuSansMono-Powerline.ttf differ diff --git a/vendor/fyne.io/fyne/v2/theme/font/EmojiOneColor.otf b/vendor/fyne.io/fyne/v2/theme/font/EmojiOneColor.otf new file mode 100644 index 0000000..f76d8b6 Binary files /dev/null and b/vendor/fyne.io/fyne/v2/theme/font/EmojiOneColor.otf differ diff --git a/vendor/fyne.io/fyne/v2/theme/font/InterSymbols-Regular.ttf b/vendor/fyne.io/fyne/v2/theme/font/InterSymbols-Regular.ttf new file mode 100644 index 0000000..c9bd742 Binary files /dev/null and b/vendor/fyne.io/fyne/v2/theme/font/InterSymbols-Regular.ttf differ diff --git a/vendor/fyne.io/fyne/v2/theme/font/NotoSans-Bold.ttf b/vendor/fyne.io/fyne/v2/theme/font/NotoSans-Bold.ttf new file mode 100644 index 0000000..ab11d31 Binary files /dev/null and b/vendor/fyne.io/fyne/v2/theme/font/NotoSans-Bold.ttf differ diff --git a/vendor/fyne.io/fyne/v2/theme/font/NotoSans-BoldItalic.ttf b/vendor/fyne.io/fyne/v2/theme/font/NotoSans-BoldItalic.ttf new file mode 100644 index 0000000..6dfd1e6 Binary files /dev/null and b/vendor/fyne.io/fyne/v2/theme/font/NotoSans-BoldItalic.ttf differ diff --git a/vendor/fyne.io/fyne/v2/theme/font/NotoSans-Italic.ttf b/vendor/fyne.io/fyne/v2/theme/font/NotoSans-Italic.ttf new file mode 100644 index 0000000..1639ad7 Binary files /dev/null and b/vendor/fyne.io/fyne/v2/theme/font/NotoSans-Italic.ttf differ diff --git a/vendor/fyne.io/fyne/v2/theme/font/NotoSans-Regular.ttf b/vendor/fyne.io/fyne/v2/theme/font/NotoSans-Regular.ttf new file mode 100644 index 0000000..a1b8994 Binary files /dev/null and b/vendor/fyne.io/fyne/v2/theme/font/NotoSans-Regular.ttf differ diff --git a/vendor/fyne.io/fyne/v2/theme/icons.go b/vendor/fyne.io/fyne/v2/theme/icons.go new file mode 100644 index 0000000..17c525c --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons.go @@ -0,0 +1,1409 @@ +package theme + +import ( + "image/color" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/svg" +) + +const ( + // IconNameCancel is the name of theme lookup for cancel icon. + // + // Since: 2.0 + IconNameCancel fyne.ThemeIconName = "cancel" + + // IconNameConfirm is the name of theme lookup for confirm icon. + // + // Since: 2.0 + IconNameConfirm fyne.ThemeIconName = "confirm" + + // IconNameDelete is the name of theme lookup for delete icon. + // + // Since: 2.0 + IconNameDelete fyne.ThemeIconName = "delete" + + // IconNameSearch is the name of theme lookup for search icon. + // + // Since: 2.0 + IconNameSearch fyne.ThemeIconName = "search" + + // IconNameSearchReplace is the name of theme lookup for search and replace icon. + // + // Since: 2.0 + IconNameSearchReplace fyne.ThemeIconName = "searchReplace" + + // IconNameMenu is the name of theme lookup for menu icon. + // + // Since: 2.0 + IconNameMenu fyne.ThemeIconName = "menu" + + // IconNameMenuExpand is the name of theme lookup for menu expansion icon. + // + // Since: 2.0 + IconNameMenuExpand fyne.ThemeIconName = "menuExpand" + + // IconNameCheckButton is the name of theme lookup for unchecked check button icon. + // + // Since: 2.0 + IconNameCheckButton fyne.ThemeIconName = "unchecked" + + // IconNameCheckButtonChecked is the name of theme lookup for checked check button icon. + // + // Since: 2.0 + IconNameCheckButtonChecked fyne.ThemeIconName = "checked" + + // IconNameCheckButtonFill is the name of theme lookup for filled check button icon. + // + // Since: 2.5 + IconNameCheckButtonFill fyne.ThemeIconName = "iconNameCheckButtonFill" + + // IconNameCheckButtonPartial is the name of theme lookup for "partially" checked check button icon. + // + // Since: 2.6 + IconNameCheckButtonPartial fyne.ThemeIconName = "partial" + + // IconNameRadioButton is the name of theme lookup for radio button unchecked icon. + // + // Since: 2.0 + IconNameRadioButton fyne.ThemeIconName = "radioButton" + + // IconNameRadioButtonChecked is the name of theme lookup for radio button checked icon. + // + // Since: 2.0 + IconNameRadioButtonChecked fyne.ThemeIconName = "radioButtonChecked" + + // IconNameRadioButtonFill is the name of theme lookup for filled radio button icon. + // + // Since: 2.5 + IconNameRadioButtonFill fyne.ThemeIconName = "iconNameRadioButtonFill" + + // IconNameColorAchromatic is the name of theme lookup for greyscale color icon. + // + // Since: 2.0 + IconNameColorAchromatic fyne.ThemeIconName = "colorAchromatic" + + // IconNameColorChromatic is the name of theme lookup for full color icon. + // + // Since: 2.0 + IconNameColorChromatic fyne.ThemeIconName = "colorChromatic" + + // IconNameColorPalette is the name of theme lookup for color palette icon. + // + // Since: 2.0 + IconNameColorPalette fyne.ThemeIconName = "colorPalette" + + // IconNameContentAdd is the name of theme lookup for content add icon. + // + // Since: 2.0 + IconNameContentAdd fyne.ThemeIconName = "contentAdd" + + // IconNameContentRemove is the name of theme lookup for content remove icon. + // + // Since: 2.0 + IconNameContentRemove fyne.ThemeIconName = "contentRemove" + + // IconNameContentCut is the name of theme lookup for content cut icon. + // + // Since: 2.0 + IconNameContentCut fyne.ThemeIconName = "contentCut" + + // IconNameContentCopy is the name of theme lookup for content copy icon. + // + // Since: 2.0 + IconNameContentCopy fyne.ThemeIconName = "contentCopy" + + // IconNameContentPaste is the name of theme lookup for content paste icon. + // + // Since: 2.0 + IconNameContentPaste fyne.ThemeIconName = "contentPaste" + + // IconNameContentClear is the name of theme lookup for content clear icon. + // + // Since: 2.0 + IconNameContentClear fyne.ThemeIconName = "contentClear" + + // IconNameContentRedo is the name of theme lookup for content redo icon. + // + // Since: 2.0 + IconNameContentRedo fyne.ThemeIconName = "contentRedo" + + // IconNameContentUndo is the name of theme lookup for content undo icon. + // + // Since: 2.0 + IconNameContentUndo fyne.ThemeIconName = "contentUndo" + + // IconNameInfo is the name of theme lookup for info icon. + // + // Since: 2.0 + IconNameInfo fyne.ThemeIconName = "info" + + // IconNameQuestion is the name of theme lookup for question icon. + // + // Since: 2.0 + IconNameQuestion fyne.ThemeIconName = "question" + + // IconNameWarning is the name of theme lookup for warning icon. + // + // Since: 2.0 + IconNameWarning fyne.ThemeIconName = "warning" + + // IconNameError is the name of theme lookup for error icon. + // + // Since: 2.0 + IconNameError fyne.ThemeIconName = "error" + + // IconNameBrokenImage is the name of the theme lookup for broken-image icon. + // + // Since: 2.4 + IconNameBrokenImage fyne.ThemeIconName = "broken-image" + + // IconNameDocument is the name of theme lookup for document icon. + // + // Since: 2.0 + IconNameDocument fyne.ThemeIconName = "document" + + // IconNameDocumentCreate is the name of theme lookup for document create icon. + // + // Since: 2.0 + IconNameDocumentCreate fyne.ThemeIconName = "documentCreate" + + // IconNameDocumentPrint is the name of theme lookup for document print icon. + // + // Since: 2.0 + IconNameDocumentPrint fyne.ThemeIconName = "documentPrint" + + // IconNameDocumentSave is the name of theme lookup for document save icon. + // + // Since: 2.0 + IconNameDocumentSave fyne.ThemeIconName = "documentSave" + + // IconNameDragCornerIndicator is the name of the icon used in inner windows to indicate a draggable corner. + // + // Since: 2.5 + IconNameDragCornerIndicator fyne.ThemeIconName = "dragCornerIndicator" + + // IconNameMoreHorizontal is the name of theme lookup for horizontal more. + // + // Since 2.0 + IconNameMoreHorizontal fyne.ThemeIconName = "moreHorizontal" + + // IconNameMoreVertical is the name of theme lookup for vertical more. + // + // Since 2.0 + IconNameMoreVertical fyne.ThemeIconName = "moreVertical" + + // IconNameMailAttachment is the name of theme lookup for mail attachment icon. + // + // Since: 2.0 + IconNameMailAttachment fyne.ThemeIconName = "mailAttachment" + + // IconNameMailCompose is the name of theme lookup for mail compose icon. + // + // Since: 2.0 + IconNameMailCompose fyne.ThemeIconName = "mailCompose" + + // IconNameMailForward is the name of theme lookup for mail forward icon. + // + // Since: 2.0 + IconNameMailForward fyne.ThemeIconName = "mailForward" + + // IconNameMailReply is the name of theme lookup for mail reply icon. + // + // Since: 2.0 + IconNameMailReply fyne.ThemeIconName = "mailReply" + + // IconNameMailReplyAll is the name of theme lookup for mail reply-all icon. + // + // Since: 2.0 + IconNameMailReplyAll fyne.ThemeIconName = "mailReplyAll" + + // IconNameMailSend is the name of theme lookup for mail send icon. + // + // Since: 2.0 + IconNameMailSend fyne.ThemeIconName = "mailSend" + + // IconNameMediaMusic is the name of theme lookup for media music icon. + // + // Since: 2.1 + IconNameMediaMusic fyne.ThemeIconName = "mediaMusic" + + // IconNameMediaPhoto is the name of theme lookup for media photo icon. + // + // Since: 2.1 + IconNameMediaPhoto fyne.ThemeIconName = "mediaPhoto" + + // IconNameMediaVideo is the name of theme lookup for media video icon. + // + // Since: 2.1 + IconNameMediaVideo fyne.ThemeIconName = "mediaVideo" + + // IconNameMediaFastForward is the name of theme lookup for media fast-forward icon. + // + // Since: 2.0 + IconNameMediaFastForward fyne.ThemeIconName = "mediaFastForward" + + // IconNameMediaFastRewind is the name of theme lookup for media fast-rewind icon. + // + // Since: 2.0 + IconNameMediaFastRewind fyne.ThemeIconName = "mediaFastRewind" + + // IconNameMediaPause is the name of theme lookup for media pause icon. + // + // Since: 2.0 + IconNameMediaPause fyne.ThemeIconName = "mediaPause" + + // IconNameMediaPlay is the name of theme lookup for media play icon. + // + // Since: 2.0 + IconNameMediaPlay fyne.ThemeIconName = "mediaPlay" + + // IconNameMediaRecord is the name of theme lookup for media record icon. + // + // Since: 2.0 + IconNameMediaRecord fyne.ThemeIconName = "mediaRecord" + + // IconNameMediaReplay is the name of theme lookup for media replay icon. + // + // Since: 2.0 + IconNameMediaReplay fyne.ThemeIconName = "mediaReplay" + + // IconNameMediaSkipNext is the name of theme lookup for media skip next icon. + // + // Since: 2.0 + IconNameMediaSkipNext fyne.ThemeIconName = "mediaSkipNext" + + // IconNameMediaSkipPrevious is the name of theme lookup for media skip previous icon. + // + // Since: 2.0 + IconNameMediaSkipPrevious fyne.ThemeIconName = "mediaSkipPrevious" + + // IconNameMediaStop is the name of theme lookup for media stop icon. + // + // Since: 2.0 + IconNameMediaStop fyne.ThemeIconName = "mediaStop" + + // IconNameMoveDown is the name of theme lookup for move down icon. + // + // Since: 2.0 + IconNameMoveDown fyne.ThemeIconName = "arrowDown" + + // IconNameMoveUp is the name of theme lookup for move up icon. + // + // Since: 2.0 + IconNameMoveUp fyne.ThemeIconName = "arrowUp" + + // IconNameNavigateBack is the name of theme lookup for navigate back icon. + // + // Since: 2.0 + IconNameNavigateBack fyne.ThemeIconName = "arrowBack" + + // IconNameNavigateNext is the name of theme lookup for navigate next icon. + // + // Since: 2.0 + IconNameNavigateNext fyne.ThemeIconName = "arrowForward" + + // IconNameArrowDropDown is the name of theme lookup for drop-down arrow icon. + // + // Since: 2.0 + IconNameArrowDropDown fyne.ThemeIconName = "arrowDropDown" + + // IconNameArrowDropUp is the name of theme lookup for drop-up arrow icon. + // + // Since: 2.0 + IconNameArrowDropUp fyne.ThemeIconName = "arrowDropUp" + + // IconNameFile is the name of theme lookup for file icon. + // + // Since: 2.0 + IconNameFile fyne.ThemeIconName = "file" + + // IconNameFileApplication is the name of theme lookup for file application icon. + // + // Since: 2.0 + IconNameFileApplication fyne.ThemeIconName = "fileApplication" + + // IconNameFileAudio is the name of theme lookup for file audio icon. + // + // Since: 2.0 + IconNameFileAudio fyne.ThemeIconName = "fileAudio" + + // IconNameFileImage is the name of theme lookup for file image icon. + // + // Since: 2.0 + IconNameFileImage fyne.ThemeIconName = "fileImage" + + // IconNameFileText is the name of theme lookup for file text icon. + // + // Since: 2.0 + IconNameFileText fyne.ThemeIconName = "fileText" + + // IconNameFileVideo is the name of theme lookup for file video icon. + // + // Since: 2.0 + IconNameFileVideo fyne.ThemeIconName = "fileVideo" + + // IconNameFolder is the name of theme lookup for folder icon. + // + // Since: 2.0 + IconNameFolder fyne.ThemeIconName = "folder" + + // IconNameFolderNew is the name of theme lookup for folder new icon. + // + // Since: 2.0 + IconNameFolderNew fyne.ThemeIconName = "folderNew" + + // IconNameFolderOpen is the name of theme lookup for folder open icon. + // + // Since: 2.0 + IconNameFolderOpen fyne.ThemeIconName = "folderOpen" + + // IconNameHelp is the name of theme lookup for help icon. + // + // Since: 2.0 + IconNameHelp fyne.ThemeIconName = "help" + + // IconNameHistory is the name of theme lookup for history icon. + // + // Since: 2.0 + IconNameHistory fyne.ThemeIconName = "history" + + // IconNameHome is the name of theme lookup for home icon. + // + // Since: 2.0 + IconNameHome fyne.ThemeIconName = "home" + + // IconNameSettings is the name of theme lookup for settings icon. + // + // Since: 2.0 + IconNameSettings fyne.ThemeIconName = "settings" + + // IconNameStorage is the name of theme lookup for storage icon. + // + // Since: 2.0 + IconNameStorage fyne.ThemeIconName = "storage" + + // IconNameUpload is the name of theme lookup for upload icon. + // + // Since: 2.0 + IconNameUpload fyne.ThemeIconName = "upload" + + // IconNameViewFullScreen is the name of theme lookup for view fullscreen icon. + // + // Since: 2.0 + IconNameViewFullScreen fyne.ThemeIconName = "viewFullScreen" + + // IconNameViewRefresh is the name of theme lookup for view refresh icon. + // + // Since: 2.0 + IconNameViewRefresh fyne.ThemeIconName = "viewRefresh" + + // IconNameViewZoomFit is the name of theme lookup for view zoom fit icon. + // + // Since: 2.0 + IconNameViewZoomFit fyne.ThemeIconName = "viewZoomFit" + + // IconNameViewZoomIn is the name of theme lookup for view zoom in icon. + // + // Since: 2.0 + IconNameViewZoomIn fyne.ThemeIconName = "viewZoomIn" + + // IconNameViewZoomOut is the name of theme lookup for view zoom out icon. + // + // Since: 2.0 + IconNameViewZoomOut fyne.ThemeIconName = "viewZoomOut" + + // IconNameViewRestore is the name of theme lookup for view restore icon. + // + // Since: 2.0 + IconNameViewRestore fyne.ThemeIconName = "viewRestore" + + // IconNameVisibility is the name of theme lookup for visibility icon. + // + // Since: 2.0 + IconNameVisibility fyne.ThemeIconName = "visibility" + + // IconNameVisibilityOff is the name of theme lookup for invisibility icon. + // + // Since: 2.0 + IconNameVisibilityOff fyne.ThemeIconName = "visibilityOff" + + // IconNameVolumeDown is the name of theme lookup for volume down icon. + // + // Since: 2.0 + IconNameVolumeDown fyne.ThemeIconName = "volumeDown" + + // IconNameVolumeMute is the name of theme lookup for volume mute icon. + // + // Since: 2.0 + IconNameVolumeMute fyne.ThemeIconName = "volumeMute" + + // IconNameVolumeUp is the name of theme lookup for volume up icon. + // + // Since: 2.0 + IconNameVolumeUp fyne.ThemeIconName = "volumeUp" + + // IconNameDownload is the name of theme lookup for download icon. + // + // Since: 2.0 + IconNameDownload fyne.ThemeIconName = "download" + + // IconNameComputer is the name of theme lookup for computer icon. + // + // Since: 2.0 + IconNameComputer fyne.ThemeIconName = "computer" + + // IconNameDesktop is the name of theme lookup for desktop icon. + // + // Since: 2.5 + IconNameDesktop fyne.ThemeIconName = "desktop" + + // IconNameAccount is the name of theme lookup for account icon. + // + // Since: 2.1 + IconNameAccount fyne.ThemeIconName = "account" + + // IconNameCalendar is the name of theme lookup for calendar icon. + // + // Since: 2.6 + IconNameCalendar fyne.ThemeIconName = "calendar" + + // IconNameLogin is the name of theme lookup for login icon. + // + // Since: 2.1 + IconNameLogin fyne.ThemeIconName = "login" + + // IconNameLogout is the name of theme lookup for logout icon. + // + // Since: 2.1 + IconNameLogout fyne.ThemeIconName = "logout" + + // IconNameList is the name of theme lookup for list icon. + // + // Since: 2.1 + IconNameList fyne.ThemeIconName = "list" + + // IconNameGrid is the name of theme lookup for grid icon. + // + // Since: 2.1 + IconNameGrid fyne.ThemeIconName = "grid" + + // IconNameWindowClose is the name of theme lookup for window close icon. + // + // Since: 2.5 + IconNameWindowClose fyne.ThemeIconName = "windowClose" + + // IconNameWindowMaximize is the name of theme lookup for window maximize icon. + // + // Since: 2.5 + IconNameWindowMaximize fyne.ThemeIconName = "windowMaximize" + + // IconNameWindowMinimize is the name of theme lookup for window minimize icon. + // + // Since: 2.5 + IconNameWindowMinimize fyne.ThemeIconName = "windowMinimize" +) + +var icons = map[fyne.ThemeIconName]fyne.Resource{ + IconNameCancel: NewThemedResource(cancelIconRes), + IconNameConfirm: NewThemedResource(checkIconRes), + IconNameDelete: NewThemedResource(deleteIconRes), + IconNameSearch: NewThemedResource(searchIconRes), + IconNameSearchReplace: NewThemedResource(searchreplaceIconRes), + IconNameMenu: NewThemedResource(menuIconRes), + IconNameMenuExpand: NewThemedResource(menuexpandIconRes), + + IconNameCheckButton: NewThemedResource(checkboxIconRes), + IconNameCheckButtonChecked: NewThemedResource(checkboxcheckedIconRes), + IconNameCheckButtonFill: NewThemedResource(checkboxfillIconRes), + IconNameCheckButtonPartial: NewThemedResource(checkboxpartialIconRes), + IconNameRadioButton: NewThemedResource(radiobuttonIconRes), + IconNameRadioButtonChecked: NewThemedResource(radiobuttoncheckedIconRes), + IconNameRadioButtonFill: NewThemedResource(radiobuttonfillIconRes), + + IconNameContentAdd: NewThemedResource(contentaddIconRes), + IconNameContentClear: NewThemedResource(cancelIconRes), + IconNameContentRemove: NewThemedResource(contentremoveIconRes), + IconNameContentCut: NewThemedResource(contentcutIconRes), + IconNameContentCopy: NewThemedResource(contentcopyIconRes), + IconNameContentPaste: NewThemedResource(contentpasteIconRes), + IconNameContentRedo: NewThemedResource(contentredoIconRes), + IconNameContentUndo: NewThemedResource(contentundoIconRes), + + IconNameColorAchromatic: NewThemedResource(colorachromaticIconRes), + IconNameColorChromatic: NewThemedResource(colorchromaticIconRes), + IconNameColorPalette: NewThemedResource(colorpaletteIconRes), + + IconNameDocument: NewThemedResource(documentIconRes), + IconNameDocumentCreate: NewThemedResource(documentcreateIconRes), + IconNameDocumentPrint: NewThemedResource(documentprintIconRes), + IconNameDocumentSave: NewThemedResource(documentsaveIconRes), + + IconNameDragCornerIndicator: NewThemedResource(dragcornerindicatorIconRes), + + IconNameMoreHorizontal: NewThemedResource(morehorizontalIconRes), + IconNameMoreVertical: NewThemedResource(moreverticalIconRes), + + IconNameInfo: NewThemedResource(infoIconRes), + IconNameQuestion: NewThemedResource(questionIconRes), + IconNameWarning: NewThemedResource(warningIconRes), + IconNameError: NewThemedResource(errorIconRes), + IconNameBrokenImage: NewThemedResource(brokenimageIconRes), + + IconNameMailAttachment: NewThemedResource(mailattachmentIconRes), + IconNameMailCompose: NewThemedResource(mailcomposeIconRes), + IconNameMailForward: NewThemedResource(mailforwardIconRes), + IconNameMailReply: NewThemedResource(mailreplyIconRes), + IconNameMailReplyAll: NewThemedResource(mailreplyallIconRes), + IconNameMailSend: NewThemedResource(mailsendIconRes), + + IconNameMediaMusic: NewThemedResource(mediamusicIconRes), + IconNameMediaPhoto: NewThemedResource(mediaphotoIconRes), + IconNameMediaVideo: NewThemedResource(mediavideoIconRes), + IconNameMediaFastForward: NewThemedResource(mediafastforwardIconRes), + IconNameMediaFastRewind: NewThemedResource(mediafastrewindIconRes), + IconNameMediaPause: NewThemedResource(mediapauseIconRes), + IconNameMediaPlay: NewThemedResource(mediaplayIconRes), + IconNameMediaRecord: NewThemedResource(mediarecordIconRes), + IconNameMediaReplay: NewThemedResource(mediareplayIconRes), + IconNameMediaSkipNext: NewThemedResource(mediaskipnextIconRes), + IconNameMediaSkipPrevious: NewThemedResource(mediaskippreviousIconRes), + IconNameMediaStop: NewThemedResource(mediastopIconRes), + + IconNameNavigateBack: NewThemedResource(arrowbackIconRes), + IconNameMoveDown: NewThemedResource(arrowdownIconRes), + IconNameNavigateNext: NewThemedResource(arrowforwardIconRes), + IconNameMoveUp: NewThemedResource(arrowupIconRes), + IconNameArrowDropDown: NewThemedResource(arrowdropdownIconRes), + IconNameArrowDropUp: NewThemedResource(arrowdropupIconRes), + + IconNameFile: NewThemedResource(fileIconRes), + IconNameFileApplication: NewThemedResource(fileapplicationIconRes), + IconNameFileAudio: NewThemedResource(fileaudioIconRes), + IconNameFileImage: NewThemedResource(fileimageIconRes), + IconNameFileText: NewThemedResource(filetextIconRes), + IconNameFileVideo: NewThemedResource(filevideoIconRes), + IconNameFolder: NewThemedResource(folderIconRes), + IconNameFolderNew: NewThemedResource(foldernewIconRes), + IconNameFolderOpen: NewThemedResource(folderopenIconRes), + IconNameHelp: NewThemedResource(helpIconRes), + IconNameHistory: NewThemedResource(historyIconRes), + IconNameHome: NewThemedResource(homeIconRes), + IconNameSettings: NewThemedResource(settingsIconRes), + + IconNameViewFullScreen: NewThemedResource(viewfullscreenIconRes), + IconNameViewRefresh: NewThemedResource(viewrefreshIconRes), + IconNameViewRestore: NewThemedResource(viewzoomfitIconRes), + IconNameViewZoomFit: NewThemedResource(viewzoomfitIconRes), + IconNameViewZoomIn: NewThemedResource(viewzoominIconRes), + IconNameViewZoomOut: NewThemedResource(viewzoomoutIconRes), + + IconNameVisibility: NewThemedResource(visibilityIconRes), + IconNameVisibilityOff: NewThemedResource(visibilityoffIconRes), + + IconNameVolumeDown: NewThemedResource(volumedownIconRes), + IconNameVolumeMute: NewThemedResource(volumemuteIconRes), + IconNameVolumeUp: NewThemedResource(volumeupIconRes), + + IconNameDownload: NewThemedResource(downloadIconRes), + IconNameComputer: NewThemedResource(computerIconRes), + IconNameDesktop: NewThemedResource(desktopIconRes), + IconNameStorage: NewThemedResource(storageIconRes), + IconNameUpload: NewThemedResource(uploadIconRes), + + IconNameAccount: NewThemedResource(accountIconRes), + IconNameCalendar: NewThemedResource(calendarIconRes), + IconNameLogin: NewThemedResource(loginIconRes), + IconNameLogout: NewThemedResource(logoutIconRes), + + IconNameList: NewThemedResource(listIconRes), + IconNameGrid: NewThemedResource(gridIconRes), + + IconNameWindowClose: NewThemedResource(cancelIconRes), + IconNameWindowMaximize: NewThemedResource(maximizeIconRes), + IconNameWindowMinimize: NewThemedResource(minimizeIconRes), +} + +// Icon looks up the specified icon for current theme. +// +// Since: 2.5 +func Icon(name fyne.ThemeIconName) fyne.Resource { + return safeIconLookup(name) +} + +// IconForWidget looks up the specified icon for requested widget using the current theme. +// If the widget theme has been overridden that theme will be used. +// +// Since: 2.5 +func IconForWidget(name fyne.ThemeIconName, w fyne.Widget) fyne.Resource { + return CurrentForWidget(w).Icon(name) +} + +func (t *builtinTheme) Icon(n fyne.ThemeIconName) fyne.Resource { + return icons[n] +} + +var _ fyne.ThemedResource = (*ThemedResource)(nil) + +// ThemedResource is a resource wrapper that will return a version of the resource with the main color changed +// for the currently selected theme. +type ThemedResource struct { + source fyne.Resource + + // ColorName specifies which theme colour should be used to theme the resource + // + // Since: 2.3 + ColorName fyne.ThemeColorName +} + +// NewColoredResource creates a resource that adapts to the current theme setting using +// the color named in the constructor. +// +// Since: 2.4 +func NewColoredResource(src fyne.Resource, name fyne.ThemeColorName) *ThemedResource { + return &ThemedResource{ + source: src, + ColorName: name, + } +} + +// NewSuccessThemedResource creates a resource that adapts to the current theme success color. +// +// Since: 2.4 +func NewSuccessThemedResource(src fyne.Resource) *ThemedResource { + return &ThemedResource{ + source: src, + ColorName: ColorNameSuccess, + } +} + +// NewThemedResource creates a resource that adapts to the current theme setting. +// By default, this will match the foreground color, but it can be changed using the `ColorName` field. +func NewThemedResource(src fyne.Resource) *ThemedResource { + return &ThemedResource{ + source: src, + } +} + +// NewWarningThemedResource creates a resource that adapts to the current theme warning color. +// +// Since: 2.4 +func NewWarningThemedResource(src fyne.Resource) *ThemedResource { + return &ThemedResource{ + source: src, + ColorName: ColorNameWarning, + } +} + +// Name returns the underlying resource name (used for caching). +func (res *ThemedResource) Name() string { + return string(res.ThemeColorName()) + "_" + unwrapResource(res.source).Name() +} + +// ThemeColorName returns the fyne.ThemeColorName that is used as foreground color. +func (res *ThemedResource) ThemeColorName() fyne.ThemeColorName { + if res.ColorName != "" { + return res.ColorName + } + + return ColorNameForeground +} + +// Content returns the underlying content of the resource adapted to the current text color. +func (res *ThemedResource) Content() []byte { + return colorizeLogError(unwrapResource(res.source).Content(), Color(res.ThemeColorName())) +} + +// Error returns a different resource for indicating an error. +func (res *ThemedResource) Error() *ErrorThemedResource { + return NewErrorThemedResource(res) +} + +var _ fyne.ThemedResource = (*InvertedThemedResource)(nil) + +// InvertedThemedResource is a resource wrapper that will return a version of the resource with the main color changed +// for use over highlighted elements. +type InvertedThemedResource struct { + source fyne.Resource +} + +// NewInvertedThemedResource creates a resource that adapts to the current theme for use over highlighted elements. +func NewInvertedThemedResource(orig fyne.Resource) *InvertedThemedResource { + res := &InvertedThemedResource{source: orig} + return res +} + +// Name returns the underlying resource name (used for caching). +func (res *InvertedThemedResource) Name() string { + return "inverted_" + unwrapResource(res.source).Name() +} + +// Content returns the underlying content of the resource adapted to the current background color. +func (res *InvertedThemedResource) Content() []byte { + clr := Color(ColorNameBackground) + return colorizeLogError(unwrapResource(res.source).Content(), clr) +} + +// ThemeColorName returns the fyne.ThemeColorName that is used as foreground color. +func (res *InvertedThemedResource) ThemeColorName() fyne.ThemeColorName { + return ColorNameBackground +} + +// Original returns the underlying resource that this inverted themed resource was adapted from +func (res *InvertedThemedResource) Original() fyne.Resource { + return res.source +} + +var _ fyne.ThemedResource = (*ErrorThemedResource)(nil) + +// ErrorThemedResource is a resource wrapper that will return a version of the resource with the main color changed +// to indicate an error. +type ErrorThemedResource struct { + source fyne.Resource +} + +// NewErrorThemedResource creates a resource that adapts to the error color for the current theme. +func NewErrorThemedResource(orig fyne.Resource) *ErrorThemedResource { + res := &ErrorThemedResource{source: orig} + return res +} + +// Name returns the underlying resource name (used for caching). +func (res *ErrorThemedResource) Name() string { + return "error_" + unwrapResource(res.source).Name() +} + +// Content returns the underlying content of the resource adapted to the current background color. +func (res *ErrorThemedResource) Content() []byte { + return colorizeLogError(unwrapResource(res.source).Content(), Color(ColorNameError)) +} + +// Original returns the underlying resource that this error themed resource was adapted from +func (res *ErrorThemedResource) Original() fyne.Resource { + return res.source +} + +// ThemeColorName returns the fyne.ThemeColorName that is used as foreground color. +// +// Since: 2.6 +func (res *ErrorThemedResource) ThemeColorName() fyne.ThemeColorName { + return ColorNameError +} + +var _ fyne.ThemedResource = (*PrimaryThemedResource)(nil) + +// PrimaryThemedResource is a resource wrapper that will return a version of the resource with the main color changed +// to the theme primary color. +type PrimaryThemedResource struct { + source fyne.Resource +} + +// NewPrimaryThemedResource creates a resource that adapts to the primary color for the current theme. +func NewPrimaryThemedResource(orig fyne.Resource) *PrimaryThemedResource { + return &PrimaryThemedResource{source: orig} +} + +// Name returns the underlying resource name (used for caching). +func (res *PrimaryThemedResource) Name() string { + return "primary_" + unwrapResource(res.source).Name() +} + +// Content returns the underlying content of the resource adapted to the current background color. +func (res *PrimaryThemedResource) Content() []byte { + return colorizeLogError(unwrapResource(res.source).Content(), Color(ColorNamePrimary)) +} + +// Original returns the underlying resource that this primary themed resource was adapted from +func (res *PrimaryThemedResource) Original() fyne.Resource { + return res.source +} + +// ThemeColorName returns the fyne.ThemeColorName that is used as foreground color. +// +// Since: 2.6 +func (res *PrimaryThemedResource) ThemeColorName() fyne.ThemeColorName { + return ColorNamePrimary +} + +var _ fyne.ThemedResource = (*DisabledResource)(nil) + +// DisabledResource is a resource wrapper that will return an appropriate resource colorized by +// the current theme's `DisabledColor` color. +type DisabledResource struct { + source fyne.Resource +} + +// Name returns the resource source name prefixed with `disabled_` (used for caching) +func (res *DisabledResource) Name() string { + return "disabled_" + unwrapResource(res.source).Name() +} + +// Content returns the disabled style content of the correct resource for the current theme +func (res *DisabledResource) Content() []byte { + return colorizeLogError(unwrapResource(res.source).Content(), Color(ColorNameDisabled)) +} + +// ThemeColorName returns the fyne.ThemeColorName that is used as foreground color. +// +// Since: 2.6 +func (res *DisabledResource) ThemeColorName() fyne.ThemeColorName { + return ColorNameDisabled +} + +// NewDisabledResource creates a resource that adapts to the current theme's DisabledColor setting. +func NewDisabledResource(res fyne.Resource) *DisabledResource { + return &DisabledResource{ + source: res, + } +} + +// FyneLogo returns a resource containing the Fyne logo. +// +// Deprecated: Applications should use their own icon in most cases. +func FyneLogo() fyne.Resource { + return fynelogo +} + +// CancelIcon returns a resource containing the standard cancel icon for the current theme +func CancelIcon() fyne.Resource { + return safeIconLookup(IconNameCancel) +} + +// ConfirmIcon returns a resource containing the standard confirm icon for the current theme +func ConfirmIcon() fyne.Resource { + return safeIconLookup(IconNameConfirm) +} + +// DeleteIcon returns a resource containing the standard delete icon for the current theme +func DeleteIcon() fyne.Resource { + return safeIconLookup(IconNameDelete) +} + +// SearchIcon returns a resource containing the standard search icon for the current theme +func SearchIcon() fyne.Resource { + return safeIconLookup(IconNameSearch) +} + +// SearchReplaceIcon returns a resource containing the standard search and replace icon for the current theme +func SearchReplaceIcon() fyne.Resource { + return safeIconLookup(IconNameSearchReplace) +} + +// MenuIcon returns a resource containing the standard (mobile) menu icon for the current theme +func MenuIcon() fyne.Resource { + return safeIconLookup(IconNameMenu) +} + +// MenuExpandIcon returns a resource containing the standard (mobile) expand "submenu icon for the current theme +func MenuExpandIcon() fyne.Resource { + return safeIconLookup(IconNameMenuExpand) +} + +// CheckButtonIcon returns a resource containing the standard checkbox icon for the current theme +func CheckButtonIcon() fyne.Resource { + return safeIconLookup(IconNameCheckButton) +} + +// CheckButtonCheckedIcon returns a resource containing the standard checkbox checked icon for the current theme +func CheckButtonCheckedIcon() fyne.Resource { + return safeIconLookup(IconNameCheckButtonChecked) +} + +// CheckButtonFillIcon returns a resource containing the filled checkbox icon for the current theme. +// +// Since: 2.5 +func CheckButtonFillIcon() fyne.Resource { + return safeIconLookup(IconNameCheckButtonFill) +} + +// RadioButtonIcon returns a resource containing the standard radio button icon for the current theme +func RadioButtonIcon() fyne.Resource { + return safeIconLookup(IconNameRadioButton) +} + +// RadioButtonCheckedIcon returns a resource containing the standard radio button checked icon for the current theme +func RadioButtonCheckedIcon() fyne.Resource { + return safeIconLookup(IconNameRadioButtonChecked) +} + +// RadioButtonFillIcon returns a resource containing the filled checkbox icon for the current theme. +// +// Since: 2.5 +func RadioButtonFillIcon() fyne.Resource { + return safeIconLookup(IconNameRadioButtonFill) +} + +// ContentAddIcon returns a resource containing the standard content add icon for the current theme +func ContentAddIcon() fyne.Resource { + return safeIconLookup(IconNameContentAdd) +} + +// ContentRemoveIcon returns a resource containing the standard content remove icon for the current theme +func ContentRemoveIcon() fyne.Resource { + return safeIconLookup(IconNameContentRemove) +} + +// ContentClearIcon returns a resource containing the standard content clear icon for the current theme +func ContentClearIcon() fyne.Resource { + return safeIconLookup(IconNameContentClear) +} + +// ContentCutIcon returns a resource containing the standard content cut icon for the current theme +func ContentCutIcon() fyne.Resource { + return safeIconLookup(IconNameContentCut) +} + +// ContentCopyIcon returns a resource containing the standard content copy icon for the current theme +func ContentCopyIcon() fyne.Resource { + return safeIconLookup(IconNameContentCopy) +} + +// ContentPasteIcon returns a resource containing the standard content paste icon for the current theme +func ContentPasteIcon() fyne.Resource { + return safeIconLookup(IconNameContentPaste) +} + +// ContentRedoIcon returns a resource containing the standard content redo icon for the current theme +func ContentRedoIcon() fyne.Resource { + return safeIconLookup(IconNameContentRedo) +} + +// ContentUndoIcon returns a resource containing the standard content undo icon for the current theme +func ContentUndoIcon() fyne.Resource { + return safeIconLookup(IconNameContentUndo) +} + +// ColorAchromaticIcon returns a resource containing the standard achromatic color icon for the current theme +func ColorAchromaticIcon() fyne.Resource { + return safeIconLookup(IconNameColorAchromatic) +} + +// ColorChromaticIcon returns a resource containing the standard chromatic color icon for the current theme +func ColorChromaticIcon() fyne.Resource { + return safeIconLookup(IconNameColorChromatic) +} + +// ColorPaletteIcon returns a resource containing the standard color palette icon for the current theme +func ColorPaletteIcon() fyne.Resource { + return safeIconLookup(IconNameColorPalette) +} + +// DocumentIcon returns a resource containing the standard document icon for the current theme +func DocumentIcon() fyne.Resource { + return safeIconLookup(IconNameDocument) +} + +// DocumentCreateIcon returns a resource containing the standard document create icon for the current theme +func DocumentCreateIcon() fyne.Resource { + return safeIconLookup(IconNameDocumentCreate) +} + +// DocumentPrintIcon returns a resource containing the standard document print icon for the current theme +func DocumentPrintIcon() fyne.Resource { + return safeIconLookup(IconNameDocumentPrint) +} + +// DocumentSaveIcon returns a resource containing the standard document save icon for the current theme +func DocumentSaveIcon() fyne.Resource { + return safeIconLookup(IconNameDocumentSave) +} + +// MoreHorizontalIcon returns a resource containing the standard horizontal more icon for the current theme +func MoreHorizontalIcon() fyne.Resource { + return Current().Icon(IconNameMoreHorizontal) +} + +// MoreVerticalIcon returns a resource containing the standard vertical more icon for the current theme +func MoreVerticalIcon() fyne.Resource { + return Current().Icon(IconNameMoreVertical) +} + +// InfoIcon returns a resource containing the standard dialog info icon for the current theme +func InfoIcon() fyne.Resource { + return safeIconLookup(IconNameInfo) +} + +// QuestionIcon returns a resource containing the standard dialog question icon for the current theme +func QuestionIcon() fyne.Resource { + return safeIconLookup(IconNameQuestion) +} + +// WarningIcon returns a resource containing the standard dialog warning icon for the current theme +func WarningIcon() fyne.Resource { + return safeIconLookup(IconNameWarning) +} + +// ErrorIcon returns a resource containing the standard dialog error icon for the current theme +func ErrorIcon() fyne.Resource { + return safeIconLookup(IconNameError) +} + +// BrokenImageIcon returns a resource containing an icon to specify a broken or missing image +// +// Since: 2.4 +func BrokenImageIcon() fyne.Resource { + return safeIconLookup(IconNameBrokenImage) +} + +// FileIcon returns a resource containing the appropriate file icon for the current theme +func FileIcon() fyne.Resource { + return safeIconLookup(IconNameFile) +} + +// FileApplicationIcon returns a resource containing the file icon representing application files for the current theme +func FileApplicationIcon() fyne.Resource { + return safeIconLookup(IconNameFileApplication) +} + +// FileAudioIcon returns a resource containing the file icon representing audio files for the current theme +func FileAudioIcon() fyne.Resource { + return safeIconLookup(IconNameFileAudio) +} + +// FileImageIcon returns a resource containing the file icon representing image files for the current theme +func FileImageIcon() fyne.Resource { + return safeIconLookup(IconNameFileImage) +} + +// FileTextIcon returns a resource containing the file icon representing text files for the current theme +func FileTextIcon() fyne.Resource { + return safeIconLookup(IconNameFileText) +} + +// FileVideoIcon returns a resource containing the file icon representing video files for the current theme +func FileVideoIcon() fyne.Resource { + return safeIconLookup(IconNameFileVideo) +} + +// FolderIcon returns a resource containing the standard folder icon for the current theme +func FolderIcon() fyne.Resource { + return safeIconLookup(IconNameFolder) +} + +// FolderNewIcon returns a resource containing the standard folder creation icon for the current theme +func FolderNewIcon() fyne.Resource { + return safeIconLookup(IconNameFolderNew) +} + +// FolderOpenIcon returns a resource containing the standard folder open icon for the current theme +func FolderOpenIcon() fyne.Resource { + return safeIconLookup(IconNameFolderOpen) +} + +// HelpIcon returns a resource containing the standard help icon for the current theme +func HelpIcon() fyne.Resource { + return safeIconLookup(IconNameHelp) +} + +// HistoryIcon returns a resource containing the standard history icon for the current theme +func HistoryIcon() fyne.Resource { + return safeIconLookup(IconNameHistory) +} + +// HomeIcon returns a resource containing the standard home folder icon for the current theme +func HomeIcon() fyne.Resource { + return safeIconLookup(IconNameHome) +} + +// SettingsIcon returns a resource containing the standard settings icon for the current theme +func SettingsIcon() fyne.Resource { + return safeIconLookup(IconNameSettings) +} + +// MailAttachmentIcon returns a resource containing the standard mail attachment icon for the current theme +func MailAttachmentIcon() fyne.Resource { + return safeIconLookup(IconNameMailAttachment) +} + +// MailComposeIcon returns a resource containing the standard mail compose icon for the current theme +func MailComposeIcon() fyne.Resource { + return safeIconLookup(IconNameMailCompose) +} + +// MailForwardIcon returns a resource containing the standard mail forward icon for the current theme +func MailForwardIcon() fyne.Resource { + return safeIconLookup(IconNameMailForward) +} + +// MailReplyIcon returns a resource containing the standard mail reply icon for the current theme +func MailReplyIcon() fyne.Resource { + return safeIconLookup(IconNameMailReply) +} + +// MailReplyAllIcon returns a resource containing the standard mail reply all icon for the current theme +func MailReplyAllIcon() fyne.Resource { + return safeIconLookup(IconNameMailReplyAll) +} + +// MailSendIcon returns a resource containing the standard mail send icon for the current theme +func MailSendIcon() fyne.Resource { + return safeIconLookup(IconNameMailSend) +} + +// MediaMusicIcon returns a resource containing the standard media music icon for the current theme +// +// Since: 2.1 +func MediaMusicIcon() fyne.Resource { + return safeIconLookup(IconNameMediaMusic) +} + +// MediaPhotoIcon returns a resource containing the standard media photo icon for the current theme +// +// Since: 2.1 +func MediaPhotoIcon() fyne.Resource { + return safeIconLookup(IconNameMediaPhoto) +} + +// MediaVideoIcon returns a resource containing the standard media video icon for the current theme +// +// Since: 2.1 +func MediaVideoIcon() fyne.Resource { + return safeIconLookup(IconNameMediaVideo) +} + +// MediaFastForwardIcon returns a resource containing the standard media fast-forward icon for the current theme +func MediaFastForwardIcon() fyne.Resource { + return safeIconLookup(IconNameMediaFastForward) +} + +// MediaFastRewindIcon returns a resource containing the standard media fast-rewind icon for the current theme +func MediaFastRewindIcon() fyne.Resource { + return safeIconLookup(IconNameMediaFastRewind) +} + +// MediaPauseIcon returns a resource containing the standard media pause icon for the current theme +func MediaPauseIcon() fyne.Resource { + return safeIconLookup(IconNameMediaPause) +} + +// MediaPlayIcon returns a resource containing the standard media play icon for the current theme +func MediaPlayIcon() fyne.Resource { + return safeIconLookup(IconNameMediaPlay) +} + +// MediaRecordIcon returns a resource containing the standard media record icon for the current theme +func MediaRecordIcon() fyne.Resource { + return safeIconLookup(IconNameMediaRecord) +} + +// MediaReplayIcon returns a resource containing the standard media replay icon for the current theme +func MediaReplayIcon() fyne.Resource { + return safeIconLookup(IconNameMediaReplay) +} + +// MediaSkipNextIcon returns a resource containing the standard media skip next icon for the current theme +func MediaSkipNextIcon() fyne.Resource { + return safeIconLookup(IconNameMediaSkipNext) +} + +// MediaSkipPreviousIcon returns a resource containing the standard media skip previous icon for the current theme +func MediaSkipPreviousIcon() fyne.Resource { + return safeIconLookup(IconNameMediaSkipPrevious) +} + +// MediaStopIcon returns a resource containing the standard media stop icon for the current theme +func MediaStopIcon() fyne.Resource { + return safeIconLookup(IconNameMediaStop) +} + +// MoveDownIcon returns a resource containing the standard down arrow icon for the current theme +func MoveDownIcon() fyne.Resource { + return safeIconLookup(IconNameMoveDown) +} + +// MoveUpIcon returns a resource containing the standard up arrow icon for the current theme +func MoveUpIcon() fyne.Resource { + return safeIconLookup(IconNameMoveUp) +} + +// NavigateBackIcon returns a resource containing the standard backward navigation icon for the current theme +func NavigateBackIcon() fyne.Resource { + return safeIconLookup(IconNameNavigateBack) +} + +// NavigateNextIcon returns a resource containing the standard forward navigation icon for the current theme +func NavigateNextIcon() fyne.Resource { + return safeIconLookup(IconNameNavigateNext) +} + +// MenuDropDownIcon returns a resource containing the standard menu drop down icon for the current theme +func MenuDropDownIcon() fyne.Resource { + return safeIconLookup(IconNameArrowDropDown) +} + +// MenuDropUpIcon returns a resource containing the standard menu drop up icon for the current theme +func MenuDropUpIcon() fyne.Resource { + return safeIconLookup(IconNameArrowDropUp) +} + +// ViewFullScreenIcon returns a resource containing the standard fullscreen icon for the current theme +func ViewFullScreenIcon() fyne.Resource { + return safeIconLookup(IconNameViewFullScreen) +} + +// ViewRestoreIcon returns a resource containing the standard exit fullscreen icon for the current theme +func ViewRestoreIcon() fyne.Resource { + return safeIconLookup(IconNameViewRestore) +} + +// ViewRefreshIcon returns a resource containing the standard refresh icon for the current theme +func ViewRefreshIcon() fyne.Resource { + return safeIconLookup(IconNameViewRefresh) +} + +// ZoomFitIcon returns a resource containing the standard zoom fit icon for the current theme +func ZoomFitIcon() fyne.Resource { + return safeIconLookup(IconNameViewZoomFit) +} + +// ZoomInIcon returns a resource containing the standard zoom in icon for the current theme +func ZoomInIcon() fyne.Resource { + return safeIconLookup(IconNameViewZoomIn) +} + +// ZoomOutIcon returns a resource containing the standard zoom out icon for the current theme +func ZoomOutIcon() fyne.Resource { + return safeIconLookup(IconNameViewZoomOut) +} + +// VisibilityIcon returns a resource containing the standard visibility icon for the current theme +func VisibilityIcon() fyne.Resource { + return safeIconLookup(IconNameVisibility) +} + +// VisibilityOffIcon returns a resource containing the standard visibility off icon for the current theme +func VisibilityOffIcon() fyne.Resource { + return safeIconLookup(IconNameVisibilityOff) +} + +// VolumeDownIcon returns a resource containing the standard volume down icon for the current theme +func VolumeDownIcon() fyne.Resource { + return safeIconLookup(IconNameVolumeDown) +} + +// VolumeMuteIcon returns a resource containing the standard volume mute icon for the current theme +func VolumeMuteIcon() fyne.Resource { + return safeIconLookup(IconNameVolumeMute) +} + +// VolumeUpIcon returns a resource containing the standard volume up icon for the current theme +func VolumeUpIcon() fyne.Resource { + return safeIconLookup(IconNameVolumeUp) +} + +// ComputerIcon returns a resource containing the standard computer icon for the current theme +func ComputerIcon() fyne.Resource { + return safeIconLookup(IconNameComputer) +} + +// DesktopIcon returns a resource containing the standard desktop icon for the current theme +func DesktopIcon() fyne.Resource { + return safeIconLookup(IconNameDesktop) +} + +// DownloadIcon returns a resource containing the standard download icon for the current theme +func DownloadIcon() fyne.Resource { + return safeIconLookup(IconNameDownload) +} + +// StorageIcon returns a resource containing the standard storage icon for the current theme +func StorageIcon() fyne.Resource { + return safeIconLookup(IconNameStorage) +} + +// UploadIcon returns a resource containing the standard upload icon for the current theme +func UploadIcon() fyne.Resource { + return safeIconLookup(IconNameUpload) +} + +// AccountIcon returns a resource containing the standard account icon for the current theme +func AccountIcon() fyne.Resource { + return safeIconLookup(IconNameAccount) +} + +// CalendarIcon returns a resource containing the standard account icon for the current theme +// +// Since: 2.6 +func CalendarIcon() fyne.Resource { + return safeIconLookup(IconNameCalendar) +} + +// LoginIcon returns a resource containing the standard login icon for the current theme +func LoginIcon() fyne.Resource { + return safeIconLookup(IconNameLogin) +} + +// LogoutIcon returns a resource containing the standard logout icon for the current theme +func LogoutIcon() fyne.Resource { + return safeIconLookup(IconNameLogout) +} + +// ListIcon returns a resource containing the standard list icon for the current theme +func ListIcon() fyne.Resource { + return safeIconLookup(IconNameList) +} + +// GridIcon returns a resource containing the standard grid icon for the current theme +func GridIcon() fyne.Resource { + return safeIconLookup(IconNameGrid) +} + +// WindowCloseIcon returns a resource containing the window close icon for the current theme +// +// Since: 2.5 +func WindowCloseIcon() fyne.Resource { + return safeIconLookup(IconNameWindowClose) +} + +// WindowMaximizeIcon returns a resource containing the window maximize icon for the current theme +// +// Since: 2.5 +func WindowMaximizeIcon() fyne.Resource { + return safeIconLookup(IconNameWindowMaximize) +} + +// WindowMinimizeIcon returns a resource containing the window minimize icon for the current theme +// +// Since: 2.5 +func WindowMinimizeIcon() fyne.Resource { + return safeIconLookup(IconNameWindowMinimize) +} + +func safeIconLookup(n fyne.ThemeIconName) fyne.Resource { + icon := Current().Icon(n) + if icon == nil { + fyne.LogError("Loaded theme returned nil icon", nil) + return fallbackIcon + } + return icon +} + +// recursively "unwraps" the source of the given resource to extract +// the "base" resource - to avoid recolorizing SVG multiple times +// if we for example have a ThemedResource wrapped in an ErrorThemedResource +func unwrapResource(res fyne.Resource) fyne.Resource { + for { + switch typedRes := res.(type) { + case *DisabledResource: + res = typedRes.source + case *ErrorThemedResource: + res = typedRes.source + case *InvertedThemedResource: + res = typedRes.source + case *PrimaryThemedResource: + res = typedRes.source + case *ThemedResource: + res = typedRes.source + default: + return res + } + } +} + +func colorizeLogError(src []byte, clr color.Color) []byte { + content, err := svg.Colorize(src, clr) + if err != nil { + fyne.LogError("", err) + } + return content +} diff --git a/vendor/fyne.io/fyne/v2/theme/icons/account.svg b/vendor/fyne.io/fyne/v2/theme/icons/account.svg new file mode 100644 index 0000000..62c0f35 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/account.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/arrow-back.svg b/vendor/fyne.io/fyne/v2/theme/icons/arrow-back.svg new file mode 100644 index 0000000..75ccce4 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/arrow-back.svg @@ -0,0 +1 @@ + diff --git a/vendor/fyne.io/fyne/v2/theme/icons/arrow-down.svg b/vendor/fyne.io/fyne/v2/theme/icons/arrow-down.svg new file mode 100644 index 0000000..f27cba0 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/arrow-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/arrow-drop-down.svg b/vendor/fyne.io/fyne/v2/theme/icons/arrow-drop-down.svg new file mode 100644 index 0000000..23f0fdd --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/arrow-drop-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/arrow-drop-up.svg b/vendor/fyne.io/fyne/v2/theme/icons/arrow-drop-up.svg new file mode 100644 index 0000000..5554514 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/arrow-drop-up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/arrow-forward.svg b/vendor/fyne.io/fyne/v2/theme/icons/arrow-forward.svg new file mode 100644 index 0000000..c46a574 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/arrow-forward.svg @@ -0,0 +1 @@ + diff --git a/vendor/fyne.io/fyne/v2/theme/icons/arrow-up.svg b/vendor/fyne.io/fyne/v2/theme/icons/arrow-up.svg new file mode 100644 index 0000000..e986606 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/arrow-up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/broken-image.svg b/vendor/fyne.io/fyne/v2/theme/icons/broken-image.svg new file mode 100644 index 0000000..1267c3c --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/broken-image.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/calendar.svg b/vendor/fyne.io/fyne/v2/theme/icons/calendar.svg new file mode 100644 index 0000000..6c448c2 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/calendar.svg @@ -0,0 +1,5 @@ + + + diff --git a/vendor/fyne.io/fyne/v2/theme/icons/cancel.svg b/vendor/fyne.io/fyne/v2/theme/icons/cancel.svg new file mode 100644 index 0000000..f7fcf80 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/cancel.svg @@ -0,0 +1 @@ + diff --git a/vendor/fyne.io/fyne/v2/theme/icons/check-box-checked.svg b/vendor/fyne.io/fyne/v2/theme/icons/check-box-checked.svg new file mode 100644 index 0000000..3917b1f --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/check-box-checked.svg @@ -0,0 +1,9 @@ + + + + diff --git a/vendor/fyne.io/fyne/v2/theme/icons/check-box-fill.svg b/vendor/fyne.io/fyne/v2/theme/icons/check-box-fill.svg new file mode 100644 index 0000000..e4a1e43 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/check-box-fill.svg @@ -0,0 +1,8 @@ + + + + diff --git a/vendor/fyne.io/fyne/v2/theme/icons/check-box-partial.svg b/vendor/fyne.io/fyne/v2/theme/icons/check-box-partial.svg new file mode 100644 index 0000000..71d4861 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/check-box-partial.svg @@ -0,0 +1,9 @@ + + + + diff --git a/vendor/fyne.io/fyne/v2/theme/icons/check-box.svg b/vendor/fyne.io/fyne/v2/theme/icons/check-box.svg new file mode 100644 index 0000000..2997acd --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/check-box.svg @@ -0,0 +1,9 @@ + + + + diff --git a/vendor/fyne.io/fyne/v2/theme/icons/check.svg b/vendor/fyne.io/fyne/v2/theme/icons/check.svg new file mode 100644 index 0000000..b09aeb7 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/check.svg @@ -0,0 +1,4 @@ + + + diff --git a/vendor/fyne.io/fyne/v2/theme/icons/color-achromatic.svg b/vendor/fyne.io/fyne/v2/theme/icons/color-achromatic.svg new file mode 100644 index 0000000..2b18e46 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/color-achromatic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/color-chromatic.svg b/vendor/fyne.io/fyne/v2/theme/icons/color-chromatic.svg new file mode 100644 index 0000000..6880a5b --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/color-chromatic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/color-palette.svg b/vendor/fyne.io/fyne/v2/theme/icons/color-palette.svg new file mode 100644 index 0000000..dab5a4c --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/color-palette.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/computer.svg b/vendor/fyne.io/fyne/v2/theme/icons/computer.svg new file mode 100644 index 0000000..cfb268e --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/computer.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/content-add.svg b/vendor/fyne.io/fyne/v2/theme/icons/content-add.svg new file mode 100644 index 0000000..9349096 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/content-add.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/content-copy.svg b/vendor/fyne.io/fyne/v2/theme/icons/content-copy.svg new file mode 100644 index 0000000..7c6b60a --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/content-copy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/content-cut.svg b/vendor/fyne.io/fyne/v2/theme/icons/content-cut.svg new file mode 100644 index 0000000..b89a0d0 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/content-cut.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/content-paste.svg b/vendor/fyne.io/fyne/v2/theme/icons/content-paste.svg new file mode 100644 index 0000000..af63a64 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/content-paste.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/content-redo.svg b/vendor/fyne.io/fyne/v2/theme/icons/content-redo.svg new file mode 100644 index 0000000..4085276 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/content-redo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/content-remove.svg b/vendor/fyne.io/fyne/v2/theme/icons/content-remove.svg new file mode 100644 index 0000000..1419b96 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/content-remove.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/content-undo.svg b/vendor/fyne.io/fyne/v2/theme/icons/content-undo.svg new file mode 100644 index 0000000..fbc9edf --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/content-undo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/delete.svg b/vendor/fyne.io/fyne/v2/theme/icons/delete.svg new file mode 100644 index 0000000..bcd82b4 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/delete.svg @@ -0,0 +1 @@ + diff --git a/vendor/fyne.io/fyne/v2/theme/icons/desktop.svg b/vendor/fyne.io/fyne/v2/theme/icons/desktop.svg new file mode 100644 index 0000000..6169a92 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/desktop.svg @@ -0,0 +1 @@ + diff --git a/vendor/fyne.io/fyne/v2/theme/icons/document-create.svg b/vendor/fyne.io/fyne/v2/theme/icons/document-create.svg new file mode 100644 index 0000000..5b54e69 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/document-create.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/document-print.svg b/vendor/fyne.io/fyne/v2/theme/icons/document-print.svg new file mode 100644 index 0000000..e5305a9 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/document-print.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/document-save.svg b/vendor/fyne.io/fyne/v2/theme/icons/document-save.svg new file mode 100644 index 0000000..3fb2a8d --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/document-save.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/document.svg b/vendor/fyne.io/fyne/v2/theme/icons/document.svg new file mode 100644 index 0000000..55f5788 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/document.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/download.svg b/vendor/fyne.io/fyne/v2/theme/icons/download.svg new file mode 100644 index 0000000..c81a359 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/download.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/drag-corner-indicator.svg b/vendor/fyne.io/fyne/v2/theme/icons/drag-corner-indicator.svg new file mode 100644 index 0000000..11e8b56 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/drag-corner-indicator.svg @@ -0,0 +1,11 @@ + + + + + diff --git a/vendor/fyne.io/fyne/v2/theme/icons/error.svg b/vendor/fyne.io/fyne/v2/theme/icons/error.svg new file mode 100644 index 0000000..ac08f3e --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/error.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/file-application.svg b/vendor/fyne.io/fyne/v2/theme/icons/file-application.svg new file mode 100644 index 0000000..1e5ec77 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/file-application.svg @@ -0,0 +1,3 @@ + + + diff --git a/vendor/fyne.io/fyne/v2/theme/icons/file-audio.svg b/vendor/fyne.io/fyne/v2/theme/icons/file-audio.svg new file mode 100644 index 0000000..fbed708 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/file-audio.svg @@ -0,0 +1,3 @@ + + + diff --git a/vendor/fyne.io/fyne/v2/theme/icons/file-image.svg b/vendor/fyne.io/fyne/v2/theme/icons/file-image.svg new file mode 100644 index 0000000..65aaf6b --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/file-image.svg @@ -0,0 +1,3 @@ + + + diff --git a/vendor/fyne.io/fyne/v2/theme/icons/file-text.svg b/vendor/fyne.io/fyne/v2/theme/icons/file-text.svg new file mode 100644 index 0000000..a2a907e --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/file-text.svg @@ -0,0 +1,3 @@ + + + diff --git a/vendor/fyne.io/fyne/v2/theme/icons/file-video.svg b/vendor/fyne.io/fyne/v2/theme/icons/file-video.svg new file mode 100644 index 0000000..0332230 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/file-video.svg @@ -0,0 +1,3 @@ + + + diff --git a/vendor/fyne.io/fyne/v2/theme/icons/file.svg b/vendor/fyne.io/fyne/v2/theme/icons/file.svg new file mode 100644 index 0000000..389a654 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/folder-new.svg b/vendor/fyne.io/fyne/v2/theme/icons/folder-new.svg new file mode 100644 index 0000000..23fa565 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/folder-new.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/folder-open.svg b/vendor/fyne.io/fyne/v2/theme/icons/folder-open.svg new file mode 100644 index 0000000..b971d73 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/folder-open.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/folder.svg b/vendor/fyne.io/fyne/v2/theme/icons/folder.svg new file mode 100644 index 0000000..6d35e17 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/folder.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/fyne.png b/vendor/fyne.io/fyne/v2/theme/icons/fyne.png new file mode 100644 index 0000000..066216d Binary files /dev/null and b/vendor/fyne.io/fyne/v2/theme/icons/fyne.png differ diff --git a/vendor/fyne.io/fyne/v2/theme/icons/grid.svg b/vendor/fyne.io/fyne/v2/theme/icons/grid.svg new file mode 100644 index 0000000..901c777 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/grid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/help.svg b/vendor/fyne.io/fyne/v2/theme/icons/help.svg new file mode 100644 index 0000000..88cb2ef --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/help.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/history.svg b/vendor/fyne.io/fyne/v2/theme/icons/history.svg new file mode 100644 index 0000000..5e9b779 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/history.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/home.svg b/vendor/fyne.io/fyne/v2/theme/icons/home.svg new file mode 100644 index 0000000..ffba26b --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/home.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/info.svg b/vendor/fyne.io/fyne/v2/theme/icons/info.svg new file mode 100644 index 0000000..0981526 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/info.svg @@ -0,0 +1,9 @@ + + + + + + + diff --git a/vendor/fyne.io/fyne/v2/theme/icons/list.svg b/vendor/fyne.io/fyne/v2/theme/icons/list.svg new file mode 100644 index 0000000..f1996ec --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/list.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/login.svg b/vendor/fyne.io/fyne/v2/theme/icons/login.svg new file mode 100644 index 0000000..17c7e44 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/login.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/logout.svg b/vendor/fyne.io/fyne/v2/theme/icons/logout.svg new file mode 100644 index 0000000..a9b3559 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/logout.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/mail-attachment.svg b/vendor/fyne.io/fyne/v2/theme/icons/mail-attachment.svg new file mode 100644 index 0000000..2cb6303 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/mail-attachment.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/mail-compose.svg b/vendor/fyne.io/fyne/v2/theme/icons/mail-compose.svg new file mode 100644 index 0000000..85150f0 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/mail-compose.svg @@ -0,0 +1 @@ + diff --git a/vendor/fyne.io/fyne/v2/theme/icons/mail-forward.svg b/vendor/fyne.io/fyne/v2/theme/icons/mail-forward.svg new file mode 100644 index 0000000..5ed9cea --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/mail-forward.svg @@ -0,0 +1 @@ + diff --git a/vendor/fyne.io/fyne/v2/theme/icons/mail-reply.svg b/vendor/fyne.io/fyne/v2/theme/icons/mail-reply.svg new file mode 100644 index 0000000..3de0413 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/mail-reply.svg @@ -0,0 +1 @@ + diff --git a/vendor/fyne.io/fyne/v2/theme/icons/mail-reply_all.svg b/vendor/fyne.io/fyne/v2/theme/icons/mail-reply_all.svg new file mode 100644 index 0000000..16af3a5 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/mail-reply_all.svg @@ -0,0 +1 @@ + diff --git a/vendor/fyne.io/fyne/v2/theme/icons/mail-send.svg b/vendor/fyne.io/fyne/v2/theme/icons/mail-send.svg new file mode 100644 index 0000000..4b8cef6 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/mail-send.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/maximize.svg b/vendor/fyne.io/fyne/v2/theme/icons/maximize.svg new file mode 100644 index 0000000..a47cfe7 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/maximize.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/media-fast-forward.svg b/vendor/fyne.io/fyne/v2/theme/icons/media-fast-forward.svg new file mode 100644 index 0000000..1d153d4 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/media-fast-forward.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/media-fast-rewind.svg b/vendor/fyne.io/fyne/v2/theme/icons/media-fast-rewind.svg new file mode 100644 index 0000000..0c755b5 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/media-fast-rewind.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/media-music.svg b/vendor/fyne.io/fyne/v2/theme/icons/media-music.svg new file mode 100644 index 0000000..7a8ff7c --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/media-music.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/media-pause.svg b/vendor/fyne.io/fyne/v2/theme/icons/media-pause.svg new file mode 100644 index 0000000..f19d033 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/media-pause.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/media-photo.svg b/vendor/fyne.io/fyne/v2/theme/icons/media-photo.svg new file mode 100644 index 0000000..330c5e7 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/media-photo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/media-play.svg b/vendor/fyne.io/fyne/v2/theme/icons/media-play.svg new file mode 100644 index 0000000..4de9b59 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/media-play.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/media-record.svg b/vendor/fyne.io/fyne/v2/theme/icons/media-record.svg new file mode 100644 index 0000000..54bcce5 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/media-record.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/media-replay.svg b/vendor/fyne.io/fyne/v2/theme/icons/media-replay.svg new file mode 100644 index 0000000..a923269 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/media-replay.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/media-skip-next.svg b/vendor/fyne.io/fyne/v2/theme/icons/media-skip-next.svg new file mode 100644 index 0000000..1968ae1 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/media-skip-next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/media-skip-previous.svg b/vendor/fyne.io/fyne/v2/theme/icons/media-skip-previous.svg new file mode 100644 index 0000000..eb530f6 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/media-skip-previous.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/media-stop.svg b/vendor/fyne.io/fyne/v2/theme/icons/media-stop.svg new file mode 100644 index 0000000..95ae4dc --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/media-stop.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/media-video.svg b/vendor/fyne.io/fyne/v2/theme/icons/media-video.svg new file mode 100644 index 0000000..963587d --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/media-video.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/menu-expand.svg b/vendor/fyne.io/fyne/v2/theme/icons/menu-expand.svg new file mode 100644 index 0000000..4876d4e --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/menu-expand.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/menu.svg b/vendor/fyne.io/fyne/v2/theme/icons/menu.svg new file mode 100644 index 0000000..fd6e253 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/menu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/minimize.svg b/vendor/fyne.io/fyne/v2/theme/icons/minimize.svg new file mode 100644 index 0000000..2b62d30 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/minimize.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/more-horizontal.svg b/vendor/fyne.io/fyne/v2/theme/icons/more-horizontal.svg new file mode 100644 index 0000000..08ef108 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/more-horizontal.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/more-vertical.svg b/vendor/fyne.io/fyne/v2/theme/icons/more-vertical.svg new file mode 100644 index 0000000..0ff172d --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/more-vertical.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/question.svg b/vendor/fyne.io/fyne/v2/theme/icons/question.svg new file mode 100644 index 0000000..075afb0 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/question.svg @@ -0,0 +1,6 @@ + + + diff --git a/vendor/fyne.io/fyne/v2/theme/icons/radio-button-checked.svg b/vendor/fyne.io/fyne/v2/theme/icons/radio-button-checked.svg new file mode 100644 index 0000000..d3344f8 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/radio-button-checked.svg @@ -0,0 +1,9 @@ + + + + diff --git a/vendor/fyne.io/fyne/v2/theme/icons/radio-button-fill.svg b/vendor/fyne.io/fyne/v2/theme/icons/radio-button-fill.svg new file mode 100644 index 0000000..bb64752 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/radio-button-fill.svg @@ -0,0 +1,9 @@ + + + + diff --git a/vendor/fyne.io/fyne/v2/theme/icons/radio-button.svg b/vendor/fyne.io/fyne/v2/theme/icons/radio-button.svg new file mode 100644 index 0000000..1d4e9fd --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/radio-button.svg @@ -0,0 +1,9 @@ + + + + diff --git a/vendor/fyne.io/fyne/v2/theme/icons/search-replace.svg b/vendor/fyne.io/fyne/v2/theme/icons/search-replace.svg new file mode 100644 index 0000000..9b57f3a --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/search-replace.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/search.svg b/vendor/fyne.io/fyne/v2/theme/icons/search.svg new file mode 100644 index 0000000..19c9df7 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/settings.svg b/vendor/fyne.io/fyne/v2/theme/icons/settings.svg new file mode 100644 index 0000000..0737ecb --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/settings.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/storage.svg b/vendor/fyne.io/fyne/v2/theme/icons/storage.svg new file mode 100644 index 0000000..ccffe33 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/storage.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/upload.svg b/vendor/fyne.io/fyne/v2/theme/icons/upload.svg new file mode 100644 index 0000000..90cbe0e --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/upload.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/view-fullscreen.svg b/vendor/fyne.io/fyne/v2/theme/icons/view-fullscreen.svg new file mode 100644 index 0000000..bc394a9 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/view-fullscreen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/view-refresh.svg b/vendor/fyne.io/fyne/v2/theme/icons/view-refresh.svg new file mode 100644 index 0000000..f1ad30d --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/view-refresh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/view-zoom-fit.svg b/vendor/fyne.io/fyne/v2/theme/icons/view-zoom-fit.svg new file mode 100644 index 0000000..b6d47b9 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/view-zoom-fit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/view-zoom-in.svg b/vendor/fyne.io/fyne/v2/theme/icons/view-zoom-in.svg new file mode 100644 index 0000000..62ef0ad --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/view-zoom-in.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/view-zoom-out.svg b/vendor/fyne.io/fyne/v2/theme/icons/view-zoom-out.svg new file mode 100644 index 0000000..5f22c70 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/view-zoom-out.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/visibility-off.svg b/vendor/fyne.io/fyne/v2/theme/icons/visibility-off.svg new file mode 100644 index 0000000..19b2f21 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/visibility-off.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/visibility.svg b/vendor/fyne.io/fyne/v2/theme/icons/visibility.svg new file mode 100644 index 0000000..4beebcf --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/visibility.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/volume-down.svg b/vendor/fyne.io/fyne/v2/theme/icons/volume-down.svg new file mode 100644 index 0000000..4c21e5e --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/volume-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/volume-mute.svg b/vendor/fyne.io/fyne/v2/theme/icons/volume-mute.svg new file mode 100644 index 0000000..5df14d8 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/volume-mute.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/volume-up.svg b/vendor/fyne.io/fyne/v2/theme/icons/volume-up.svg new file mode 100644 index 0000000..6ffd257 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/volume-up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/vendor/fyne.io/fyne/v2/theme/icons/warning.svg b/vendor/fyne.io/fyne/v2/theme/icons/warning.svg new file mode 100644 index 0000000..ad684e9 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/icons/warning.svg @@ -0,0 +1,8 @@ + + + + + + + diff --git a/vendor/fyne.io/fyne/v2/theme/json.go b/vendor/fyne.io/fyne/v2/theme/json.go new file mode 100644 index 0000000..df36aaa --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/json.go @@ -0,0 +1,202 @@ +package theme + +import ( + "encoding/hex" + "encoding/json" + "errors" + "image/color" + "io" + "strings" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/storage" +) + +// FromJSON returns a Theme created from the given JSON metadata. +// Any values not present in the data will fall back to the default theme. +// If a parse error occurs it will be returned along with a default theme. +// +// Since: 2.2 +func FromJSON(data string) (fyne.Theme, error) { + return FromJSONReader(strings.NewReader(data)) +} + +// FromJSONWithFallback returns a Theme created from the given JSON metadata. +// Any values not present in the data will fall back to the specified theme. +// If a parse error occurs it will be returned along with a specified fallback theme. +// +// Since: 2.7 +func FromJSONWithFallback(data string, fallback fyne.Theme) (fyne.Theme, error) { + return fromJSONWithFallback(strings.NewReader(data), fallback) +} + +// FromJSONReader returns a Theme created from the given JSON metadata through the reader. +// Any values not present in the data will fall back to the default theme. +// If a parse error occurs it will be returned along with a default theme. +// +// Since: 2.2 +func FromJSONReader(r io.Reader) (fyne.Theme, error) { + return fromJSONWithFallback(r, DefaultTheme()) +} + +// FromJSONReaderWithFallback returns a Theme created from the given JSON metadata through the reader. +// Any values not present in the data will fall back to the specified theme. +// If a parse error occurs it will be returned along with a specified fallback theme. +// +// Since: 2.7 +func FromJSONReaderWithFallback(r io.Reader, fallback fyne.Theme) (fyne.Theme, error) { + return fromJSONWithFallback(r, fallback) +} + +func fromJSONWithFallback(r io.Reader, fallback fyne.Theme) (fyne.Theme, error) { + var th *schema + if err := json.NewDecoder(r).Decode(&th); err != nil { + return fallback, err + } + + return &jsonTheme{data: th, fallback: fallback}, nil +} + +type jsonColor struct { + color color.Color +} + +func (h *jsonColor) UnmarshalJSON(b []byte) error { + var str string + if err := json.Unmarshal(b, &str); err != nil { + return err + } + return h.parseColor(str) +} + +func (h *jsonColor) parseColor(str string) error { + data := str + switch len([]rune(str)) { + case 8, 6: + case 9, 7: // remove # prefix + data = str[1:] + case 5: // remove # prefix, then double up + data = str[1:] + fallthrough + case 4: // could be rgba or #rgb + if data[0] == '#' { + v := []rune(data[1:]) + data = string([]rune{v[0], v[0], v[1], v[1], v[2], v[2]}) + break + } + + v := []rune(data) + data = string([]rune{v[0], v[0], v[1], v[1], v[2], v[2], v[3], v[3]}) + case 3: + v := []rune(str) + data = string([]rune{v[0], v[0], v[1], v[1], v[2], v[2]}) + default: + h.color = color.Transparent + return errors.New("invalid color format: " + str) + } + + digits, err := hex.DecodeString(data) + if err != nil { + return err + } + ret := &color.NRGBA{R: digits[0], G: digits[1], B: digits[2]} + if len(digits) == 4 { + ret.A = digits[3] + } else { + ret.A = 0xff + } + + h.color = ret + return nil +} + +type uriString string + +func (u uriString) resource() fyne.Resource { + uri, err := storage.ParseURI(string(u)) + if err != nil { + fyne.LogError("Failed to parse URI", err) + return nil + } + r, err := storage.LoadResourceFromURI(uri) + if err != nil { + fyne.LogError("Failed to load resource from URI", err) + return nil + } + return r +} + +type schema struct { + Colors map[string]jsonColor `json:"Colors,omitempty"` + DarkColors map[string]jsonColor `json:"Colors-dark,omitempty"` + LightColors map[string]jsonColor `json:"Colors-light,omitempty"` + Sizes map[string]float32 `json:"Sizes,omitempty"` + + Fonts map[string]uriString `json:"Fonts,omitempty"` + Icons map[string]uriString `json:"Icons,omitempty"` +} + +type jsonTheme struct { + data *schema + fallback fyne.Theme +} + +func (t *jsonTheme) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color { + switch variant { + case VariantLight: + if val, ok := t.data.LightColors[string(name)]; ok { + return val.color + } + case VariantDark: + if val, ok := t.data.DarkColors[string(name)]; ok { + return val.color + } + } + + if val, ok := t.data.Colors[string(name)]; ok { + return val.color + } + + return t.fallback.Color(name, variant) +} + +func (t *jsonTheme) Font(style fyne.TextStyle) fyne.Resource { + if val, ok := t.data.Fonts[styleString(style)]; ok { + r := val.resource() + if r != nil { + return r + } + } + return t.fallback.Font(style) +} + +func (t *jsonTheme) Icon(name fyne.ThemeIconName) fyne.Resource { + if val, ok := t.data.Icons[string(name)]; ok { + r := val.resource() + if r != nil { + return r + } + } + return t.fallback.Icon(name) +} + +func (t *jsonTheme) Size(name fyne.ThemeSizeName) float32 { + if val, ok := t.data.Sizes[string(name)]; ok { + return val + } + + return t.fallback.Size(name) +} + +func styleString(s fyne.TextStyle) string { + if s.Bold { + if s.Italic { + return "boldItalic" + } + return "bold" + } + if s.Monospace { + return "monospace" + } + return "regular" +} diff --git a/vendor/fyne.io/fyne/v2/theme/legacy.go b/vendor/fyne.io/fyne/v2/theme/legacy.go new file mode 100644 index 0000000..bff1342 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/legacy.go @@ -0,0 +1,89 @@ +package theme + +import ( + "image/color" + + "fyne.io/fyne/v2" +) + +// FromLegacy returns a 2.0 Theme created from the given LegacyTheme data. +// This is a transition path and will be removed in the future (probably version 3.0). +// +// Since: 2.0 +func FromLegacy(t fyne.LegacyTheme) fyne.Theme { + return &legacyWrapper{old: t} +} + +var _ fyne.Theme = (*legacyWrapper)(nil) + +type legacyWrapper struct { + old fyne.LegacyTheme +} + +func (l *legacyWrapper) Color(n fyne.ThemeColorName, v fyne.ThemeVariant) color.Color { + switch n { + case ColorNameBackground: + return l.old.BackgroundColor() + case ColorNameForeground: + return l.old.TextColor() + case ColorNameButton: + return l.old.ButtonColor() + case ColorNameDisabledButton: + return l.old.DisabledButtonColor() + case ColorNameDisabled: + return l.old.DisabledTextColor() + case ColorNameFocus: + return l.old.FocusColor() + case ColorNameHover: + return l.old.HoverColor() + case ColorNamePlaceHolder: + return l.old.PlaceHolderColor() + case ColorNamePrimary: + return l.old.PrimaryColor() + case ColorNameScrollBar: + return l.old.ScrollBarColor() + case ColorNameScrollBarBackground: + return l.old.BackgroundColor() + case ColorNameShadow: + return l.old.ShadowColor() + default: + return DefaultTheme().Color(n, v) + } +} + +func (l *legacyWrapper) Font(s fyne.TextStyle) fyne.Resource { + if s.Monospace { + return l.old.TextMonospaceFont() + } + if s.Bold { + if s.Italic { + return l.old.TextBoldItalicFont() + } + return l.old.TextBoldFont() + } + if s.Italic { + return l.old.TextItalicFont() + } + return l.old.TextFont() +} + +func (l *legacyWrapper) Icon(n fyne.ThemeIconName) fyne.Resource { + return DefaultTheme().Icon(n) +} + +func (l *legacyWrapper) Size(n fyne.ThemeSizeName) float32 { + switch n { + case SizeNameInlineIcon: + return float32(l.old.IconInlineSize()) + case SizeNamePadding: + return float32(l.old.Padding()) + case SizeNameScrollBar: + return float32(l.old.ScrollBarSize()) + case SizeNameScrollBarSmall: + return float32(l.old.ScrollBarSmallSize()) + case SizeNameText: + return float32(l.old.TextSize()) + default: + return DefaultTheme().Size(n) + } +} diff --git a/vendor/fyne.io/fyne/v2/theme/size.go b/vendor/fyne.io/fyne/v2/theme/size.go new file mode 100644 index 0000000..08ca3a8 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/size.go @@ -0,0 +1,247 @@ +package theme + +import "fyne.io/fyne/v2" + +const ( + // SizeNameCaptionText is the name of theme lookup for helper text size, normally smaller than regular text size. + // + // Since: 2.0 + SizeNameCaptionText fyne.ThemeSizeName = "helperText" + + // SizeNameInlineIcon is the name of theme lookup for inline icons size. + // + // Since: 2.0 + SizeNameInlineIcon fyne.ThemeSizeName = "iconInline" + + // SizeNameInnerPadding is the name of theme lookup for internal widget padding size. + // + // Since: 2.3 + SizeNameInnerPadding fyne.ThemeSizeName = "innerPadding" + + // SizeNameLineSpacing is the name of theme lookup for between text line spacing. + // + // Since: 2.3 + SizeNameLineSpacing fyne.ThemeSizeName = "lineSpacing" + + // SizeNamePadding is the name of theme lookup for padding size. + // + // Since: 2.0 + SizeNamePadding fyne.ThemeSizeName = "padding" + + // SizeNameScrollBar is the name of theme lookup for the scrollbar size. + // + // Since: 2.0 + SizeNameScrollBar fyne.ThemeSizeName = "scrollBar" + + // SizeNameScrollBarSmall is the name of theme lookup for the shrunk scrollbar size. + // + // Since: 2.0 + SizeNameScrollBarSmall fyne.ThemeSizeName = "scrollBarSmall" + + // SizeNameSeparatorThickness is the name of theme lookup for the thickness of a separator. + // + // Since: 2.0 + SizeNameSeparatorThickness fyne.ThemeSizeName = "separator" + + // SizeNameText is the name of theme lookup for text size. + // + // Since: 2.0 + SizeNameText fyne.ThemeSizeName = "text" + + // SizeNameHeadingText is the name of theme lookup for text size of a heading. + // + // Since: 2.1 + SizeNameHeadingText fyne.ThemeSizeName = "headingText" + + // SizeNameSubHeadingText is the name of theme lookup for text size of a sub-heading. + // + // Since: 2.1 + SizeNameSubHeadingText fyne.ThemeSizeName = "subHeadingText" + + // SizeNameInputBorder is the name of theme lookup for input border size. + // + // Since: 2.0 + SizeNameInputBorder fyne.ThemeSizeName = "inputBorder" + + // SizeNameInputRadius is the name of theme lookup for input corner radius. + // + // Since: 2.4 + SizeNameInputRadius fyne.ThemeSizeName = "inputRadius" + + // SizeNameSelectionRadius is the name of theme lookup for selection corner radius. + // + // Since: 2.4 + SizeNameSelectionRadius fyne.ThemeSizeName = "selectionRadius" + + // SizeNameScrollBarRadius is the name of theme lookup for the scroll bar corner radius. + // + // Since: 2.5 + SizeNameScrollBarRadius fyne.ThemeSizeName = "scrollBarRadius" + + // SizeNameWindowButtonHeight is the name of the height for an inner window titleBar button. + // + // Since: 2.6 + SizeNameWindowButtonHeight fyne.ThemeSizeName = "windowButtonHeight" + + // SizeNameWindowButtonRadius is the name of the radius for an inner window titleBar button. + // + // Since: 2.6 + SizeNameWindowButtonRadius fyne.ThemeSizeName = "windowButtonRadius" + + // SizeNameWindowButtonIcon is the name of the width of an inner window titleBar button. + // + // Since: 2.6 + SizeNameWindowButtonIcon fyne.ThemeSizeName = "windowButtonIcon" + + // SizeNameWindowTitleBarHeight is the height for inner window titleBars. + // + // Since: 2.6 + SizeNameWindowTitleBarHeight fyne.ThemeSizeName = "windowTitleBarHeight" +) + +// CaptionTextSize returns the size for caption text. +func CaptionTextSize() float32 { + return Current().Size(SizeNameCaptionText) +} + +// IconInlineSize is the standard size of icons which appear within buttons, labels etc. +func IconInlineSize() float32 { + return Current().Size(SizeNameInlineIcon) +} + +// InnerPadding is the standard gap between element content and the outside edge of a widget. +// +// Since: 2.3 +func InnerPadding() float32 { + return Current().Size(SizeNameInnerPadding) +} + +// InputBorderSize returns the input border size (or underline size for an entry). +// +// Since: 2.0 +func InputBorderSize() float32 { + return Current().Size(SizeNameInputBorder) +} + +// InputRadiusSize returns the input radius size. +// +// Since: 2.4 +func InputRadiusSize() float32 { + return Current().Size(SizeNameInputRadius) +} + +// LineSpacing is the default gap between multiple lines of text. +// +// Since: 2.3 +func LineSpacing() float32 { + return Current().Size(SizeNameLineSpacing) +} + +// Padding is the standard gap between elements and the border around interface elements. +func Padding() float32 { + return Current().Size(SizeNamePadding) +} + +// ScrollBarSize is the width (or height) of the bars on a ScrollContainer. +func ScrollBarSize() float32 { + return Current().Size(SizeNameScrollBar) +} + +// ScrollBarSmallSize is the width (or height) of the minimized bars on a ScrollContainer. +func ScrollBarSmallSize() float32 { + return Current().Size(SizeNameScrollBarSmall) +} + +// SelectionRadiusSize returns the selection highlight radius size. +// +// Since: 2.4 +func SelectionRadiusSize() float32 { + return Current().Size(SizeNameSelectionRadius) +} + +// SeparatorThicknessSize is the standard thickness of the separator widget. +// +// Since: 2.0 +func SeparatorThicknessSize() float32 { + return Current().Size(SizeNameSeparatorThickness) +} + +// Size looks up the specified size for current theme. +// +// Since: 2.5 +func Size(name fyne.ThemeSizeName) float32 { + return Current().Size(name) +} + +// SizeForWidget looks up the specified size for the requested widget using the current theme. +// If the widget theme has been overridden that theme will be used. +// +// Since: 2.5 +func SizeForWidget(name fyne.ThemeSizeName, w fyne.Widget) float32 { + return CurrentForWidget(w).Size(name) +} + +// TextHeadingSize returns the text size for header text. +// +// Since: 2.1 +func TextHeadingSize() float32 { + return Current().Size(SizeNameHeadingText) +} + +// TextSize returns the standard text size. +func TextSize() float32 { + return Current().Size(SizeNameText) +} + +// TextSubHeadingSize returns the text size for sub-header text. +// +// Since: 2.1 +func TextSubHeadingSize() float32 { + return Current().Size(SizeNameSubHeadingText) +} + +func (t *builtinTheme) Size(s fyne.ThemeSizeName) float32 { + switch s { + case SizeNameSeparatorThickness: + return 1 + case SizeNameInlineIcon: + return 20 + case SizeNameInnerPadding: + return 8 + case SizeNameLineSpacing: + return 4 + case SizeNamePadding: + return 4 + case SizeNameScrollBar: + return 12 + case SizeNameScrollBarSmall: + return 3 + case SizeNameText: + return 14 + case SizeNameHeadingText: + return 24 + case SizeNameSubHeadingText: + return 18 + case SizeNameCaptionText: + return 11 + case SizeNameInputBorder: + return 1 + case SizeNameInputRadius: + return 5 + case SizeNameSelectionRadius: + return 3 + case SizeNameScrollBarRadius: + return 3 + case SizeNameWindowButtonHeight: + return 16 + case SizeNameWindowButtonRadius: + return 8 + case SizeNameWindowButtonIcon: + return 14 + case SizeNameWindowTitleBarHeight: + return 26 + + default: + return 0 + } +} diff --git a/vendor/fyne.io/fyne/v2/theme/theme.go b/vendor/fyne.io/fyne/v2/theme/theme.go new file mode 100644 index 0000000..22deda2 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/theme.go @@ -0,0 +1,356 @@ +// Package theme defines how a Fyne app should look when rendered. +package theme // import "fyne.io/fyne/v2/theme" + +import ( + "image/color" + "os" + "strings" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/cache" + internaltheme "fyne.io/fyne/v2/internal/theme" +) + +// Keep in mind to add new constants to the tests at test/theme.go. +const ( + // VariantDark is the version of a theme that satisfies a user preference for a dark look. + // + // Since: 2.0 + VariantDark = internaltheme.VariantDark + + // VariantLight is the version of a theme that satisfies a user preference for a light look. + // + // Since: 2.0 + VariantLight = internaltheme.VariantLight +) + +var defaultTheme, systemTheme fyne.Theme + +// DarkTheme defines the built-in dark theme colors and sizes. +// +// Deprecated: This method ignores user preference and should not be used, it will be removed in v3.0. +// If developers want to ignore user preference for theme variant they can set a custom theme. +func DarkTheme() fyne.Theme { + theme := &builtinTheme{variant: VariantDark} + + theme.initFonts() + return theme +} + +// DefaultTheme returns a built-in theme that can adapt to the user preference of light or dark colors. +// +// Since: 2.0 +func DefaultTheme() fyne.Theme { + if defaultTheme == nil { + defaultTheme = setupDefaultTheme() + } + + // check system too + if systemTheme != nil { + return systemTheme + } + + return defaultTheme +} + +// LightTheme defines the built-in light theme colors and sizes. +// +// Deprecated: This method ignores user preference and should not be used, it will be removed in v3.0. +// If developers want to ignore user preference for theme variant they can set a custom theme. +func LightTheme() fyne.Theme { + theme := &builtinTheme{variant: VariantLight} + + theme.initFonts() + return theme +} + +type builtinTheme struct { + variant fyne.ThemeVariant + + regular, bold, italic, boldItalic, monospace, symbol fyne.Resource +} + +func (t *builtinTheme) initFonts() { + t.regular = regular + t.bold = bold + t.italic = italic + t.boldItalic = bolditalic + t.monospace = monospace + t.symbol = symbol + + font := os.Getenv("FYNE_FONT") + if font != "" { + t.regular = loadCustomFont(font, "Regular", regular) + if t.regular == regular { // failed to load + t.bold = loadCustomFont(font, "Bold", bold) + t.italic = loadCustomFont(font, "Italic", italic) + t.boldItalic = loadCustomFont(font, "BoldItalic", bolditalic) + } else { // first custom font loaded, fall back to that + t.bold = loadCustomFont(font, "Bold", t.regular) + t.italic = loadCustomFont(font, "Italic", t.regular) + t.boldItalic = loadCustomFont(font, "BoldItalic", t.regular) + } + } + font = os.Getenv("FYNE_FONT_MONOSPACE") + if font != "" { + t.monospace = loadCustomFont(font, "Regular", monospace) + } + font = os.Getenv("FYNE_FONT_SYMBOL") + if font != "" { + t.symbol = loadCustomFont(font, "Regular", symbol) + } +} + +func (t *builtinTheme) Color(n fyne.ThemeColorName, v fyne.ThemeVariant) color.Color { + if t.variant != internaltheme.VariantNameUserPreference { + v = t.variant + } + + primary := fyne.CurrentApp().Settings().PrimaryColor() + if n == ColorNamePrimary || n == ColorNameHyperlink { + return internaltheme.PrimaryColorNamed(primary) + } else if n == ColorNameForegroundOnPrimary { + return internaltheme.ForegroundOnPrimaryColorNamed(primary) + } else if n == ColorNameFocus { + return focusColorNamed(primary) + } else if n == ColorNameSelection { + return selectionColorNamed(primary) + } + + if v == VariantLight { + return lightPaletteColorNamed(n) + } + + return darkPaletteColorNamed(n) +} + +func (t *builtinTheme) Font(style fyne.TextStyle) fyne.Resource { + if style.Monospace { + return t.monospace + } + if style.Bold { + if style.Italic { + return t.boldItalic + } + return t.bold + } + if style.Italic { + return t.italic + } + if style.Symbol { + return t.symbol + } + return t.regular +} + +// Current returns the theme that is currently used for the running application. +// It looks up based on user preferences and application configuration. +// +// Since: 2.5 +func Current() fyne.Theme { + app := fyne.CurrentApp() + if app == nil { + return DarkTheme() + } + currentTheme := app.Settings().Theme() + if currentTheme == nil { + return DarkTheme() + } + + return internaltheme.CurrentlyRenderingWithFallback(currentTheme) +} + +// CurrentForWidget returns the theme that is currently used for the specified widget. +// It looks for widget overrides and falls back to the application's current theme. +// +// Since: 2.5 +func CurrentForWidget(w fyne.CanvasObject) fyne.Theme { + if custom := cache.WidgetTheme(w); custom != nil { + return custom + } + + return Current() +} + +func currentVariant() fyne.ThemeVariant { + if std, ok := Current().(*builtinTheme); ok { + if std.variant != internaltheme.VariantNameUserPreference { + return std.variant // override if using the old LightTheme() or DarkTheme() constructor + } + } + + return fyne.CurrentApp().Settings().ThemeVariant() +} + +func darkPaletteColorNamed(name fyne.ThemeColorName) color.Color { + switch name { + case ColorNameBackground: + return colorDarkBackground + case ColorNameButton: + return colorDarkButton + case ColorNameDisabled: + return colorDarkDisabled + case ColorNameDisabledButton: + return colorDarkDisabledButton + case ColorNameError: + return colorDarkError + case ColorNameForeground: + return colorDarkForeground + case ColorNameForegroundOnError: + return colorDarkForegroundOnError + case ColorNameForegroundOnSuccess: + return colorDarkForegroundOnSuccess + case ColorNameForegroundOnWarning: + return colorDarkForegroundOnWarning + case ColorNameHover: + return colorDarkHover + case ColorNameHeaderBackground: + return colorDarkHeaderBackground + case ColorNameInputBackground: + return colorDarkInputBackground + case ColorNameInputBorder: + return colorDarkInputBorder + case ColorNameMenuBackground: + return colorDarkMenuBackground + case ColorNameOverlayBackground: + return colorDarkOverlayBackground + case ColorNamePlaceHolder: + return colorDarkPlaceholder + case ColorNamePressed: + return colorDarkPressed + case ColorNameScrollBar: + return colorDarkScrollBar + case ColorNameScrollBarBackground: + return colorDarkScrollBarBackground + case ColorNameSeparator: + return colorDarkSeparator + case ColorNameShadow: + return colorDarkShadow + case ColorNameSuccess: + return colorDarkSuccess + case ColorNameWarning: + return colorDarkWarning + } + + return color.Transparent +} + +func focusColorNamed(name string) color.NRGBA { + switch name { + case ColorRed: + return colorLightFocusRed + case ColorOrange: + return colorLightFocusOrange + case ColorYellow: + return colorLightFocusYellow + case ColorGreen: + return colorLightFocusGreen + case ColorPurple: + return colorLightFocusPurple + case ColorBrown: + return colorLightFocusBrown + case ColorGray: + return colorLightFocusGray + } + + // We return the value for ColorBlue for every other value. + // There is no need to have it in the switch above. + return colorLightFocusBlue +} + +func lightPaletteColorNamed(name fyne.ThemeColorName) color.Color { + switch name { + case ColorNameBackground: + return colorLightBackground + case ColorNameButton: + return colorLightButton + case ColorNameDisabled: + return colorLightDisabled + case ColorNameDisabledButton: + return colorLightDisabledButton + case ColorNameError: + return colorLightError + case ColorNameForeground: + return colorLightForeground + case ColorNameForegroundOnError: + return colorLightForegroundOnError + case ColorNameForegroundOnSuccess: + return colorLightForegroundOnSuccess + case ColorNameForegroundOnWarning: + return colorLightForegroundOnWarning + case ColorNameHover: + return colorLightHover + case ColorNameHeaderBackground: + return colorLightHeaderBackground + case ColorNameInputBackground: + return colorLightInputBackground + case ColorNameInputBorder: + return colorLightInputBorder + case ColorNameMenuBackground: + return colorLightMenuBackground + case ColorNameOverlayBackground: + return colorLightOverlayBackground + case ColorNamePlaceHolder: + return colorLightPlaceholder + case ColorNamePressed: + return colorLightPressed + case ColorNameScrollBar: + return colorLightScrollBar + case ColorNameScrollBarBackground: + return colorLightScrollBarBackground + case ColorNameSeparator: + return colorLightSeparator + case ColorNameShadow: + return colorLightShadow + case ColorNameSuccess: + return colorLightSuccess + case ColorNameWarning: + return colorLightWarning + } + + return color.Transparent +} + +func loadCustomFont(env, variant string, fallback fyne.Resource) fyne.Resource { + variantPath := strings.ReplaceAll(env, "Regular", variant) + + res, err := fyne.LoadResourceFromPath(variantPath) + if err != nil { + fyne.LogError("Error loading specified font", err) + return fallback + } + + return res +} + +func selectionColorNamed(name string) color.NRGBA { + switch name { + case ColorRed: + return colorLightSelectionRed + case ColorOrange: + return colorLightSelectionOrange + case ColorYellow: + return colorLightSelectionYellow + case ColorGreen: + return colorLightSelectionGreen + case ColorPurple: + return colorLightSelectionPurple + case ColorBrown: + return colorLightSelectionBrown + case ColorGray: + return colorLightSelectionGray + } + + // We return the value for ColorBlue for every other value. + // There is no need to have it in the switch above. + return colorLightSelectionBlue +} + +func setupDefaultTheme() fyne.Theme { + theme := &builtinTheme{variant: internaltheme.VariantNameUserPreference} + theme.initFonts() + + systemTheme = setupSystemTheme(theme) + + return theme +} diff --git a/vendor/fyne.io/fyne/v2/theme/theme_desktop.go b/vendor/fyne.io/fyne/v2/theme/theme_desktop.go new file mode 100644 index 0000000..d614f89 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/theme_desktop.go @@ -0,0 +1,32 @@ +//go:build !android && !ios && !mobile && !wasm && !test_web_driver + +package theme + +import ( + "bufio" + "os" + "path/filepath" + + "fyne.io/fyne/v2" + internalApp "fyne.io/fyne/v2/internal/app" +) + +func setupSystemTheme(fallback fyne.Theme) fyne.Theme { + path := filepath.Join(internalApp.RootConfigDir(), "theme.json") + f, err := os.Open(path) + if err != nil { + if !os.IsNotExist(err) { + fyne.LogError("Failed to load user theme file: "+path, err) + } + return nil + } + defer f.Close() + + th, err := fromJSONWithFallback(bufio.NewReader(f), fallback) + if err != nil { + fyne.LogError("Failed to parse user theme file: "+path, err) + return nil + } + + return th +} diff --git a/vendor/fyne.io/fyne/v2/theme/theme_hints.go b/vendor/fyne.io/fyne/v2/theme/theme_hints.go new file mode 100644 index 0000000..2c279e3 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/theme_hints.go @@ -0,0 +1,8 @@ +//go:build hints + +package theme + +var ( + fallbackColor = colorLightError + fallbackIcon = NewErrorThemedResource(errorIconRes) +) diff --git a/vendor/fyne.io/fyne/v2/theme/theme_mobile.go b/vendor/fyne.io/fyne/v2/theme/theme_mobile.go new file mode 100644 index 0000000..69de9e1 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/theme_mobile.go @@ -0,0 +1,9 @@ +//go:build android || ios || mobile + +package theme + +import "fyne.io/fyne/v2" + +func setupSystemTheme(fallback fyne.Theme) fyne.Theme { + return fallback +} diff --git a/vendor/fyne.io/fyne/v2/theme/theme_other.go b/vendor/fyne.io/fyne/v2/theme/theme_other.go new file mode 100644 index 0000000..e12d351 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/theme_other.go @@ -0,0 +1,14 @@ +//go:build !hints + +package theme + +import ( + "image/color" + + "fyne.io/fyne/v2" +) + +var ( + fallbackColor = color.Transparent + fallbackIcon = &fyne.StaticResource{} +) diff --git a/vendor/fyne.io/fyne/v2/theme/theme_wasm.go b/vendor/fyne.io/fyne/v2/theme/theme_wasm.go new file mode 100644 index 0000000..32de537 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/theme_wasm.go @@ -0,0 +1,9 @@ +//go:build wasm || test_web_driver + +package theme + +import "fyne.io/fyne/v2" + +func setupSystemTheme(fallback fyne.Theme) fyne.Theme { + return fallback +} diff --git a/vendor/fyne.io/fyne/v2/theme/unbundled-emoji.go b/vendor/fyne.io/fyne/v2/theme/unbundled-emoji.go new file mode 100644 index 0000000..99665e7 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/theme/unbundled-emoji.go @@ -0,0 +1,7 @@ +//go:build no_emoji + +package theme + +import "fyne.io/fyne/v2" + +var emoji fyne.Resource diff --git a/vendor/fyne.io/fyne/v2/thread.go b/vendor/fyne.io/fyne/v2/thread.go new file mode 100644 index 0000000..203c390 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/thread.go @@ -0,0 +1,20 @@ +package fyne + +// DoAndWait is used to execute a specified function in the main Fyne runtime context. +// This is required when a background process wishes to adjust graphical elements of a running app. +// Developers should use this only from within goroutines they have created. +// +// Since: 2.6 +func DoAndWait(fn func()) { + CurrentApp().Driver().DoFromGoroutine(fn, true) +} + +// Do is used to execute a specified function in the main Fyne runtime context without waiting. +// This is required when a background process wishes to adjust graphical elements of a running app. +// Developers should use this only from within goroutines they have created and when the result does not have to +// be waited for. +// +// Since: 2.6 +func Do(fn func()) { + CurrentApp().Driver().DoFromGoroutine(fn, false) +} diff --git a/vendor/fyne.io/fyne/v2/uri.go b/vendor/fyne.io/fyne/v2/uri.go new file mode 100644 index 0000000..0991cb6 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/uri.go @@ -0,0 +1,103 @@ +package fyne + +import ( + "fmt" + "io" +) + +// URIReadCloser represents a cross platform data stream from a file or provider of data. +// It may refer to an item on a filesystem or data in another application that we have access to. +type URIReadCloser interface { + io.ReadCloser + + URI() URI +} + +// URIWriteCloser represents a cross platform data writer for a file resource. +// This will normally refer to a local file resource. +type URIWriteCloser interface { + io.WriteCloser + + URI() URI +} + +// URI represents the identifier of a resource on a target system. This +// resource may be a file or another data source such as an app or file sharing +// system. The URI represents an absolute location of a resource, it is up to any +// parse or constructor implementations to ensure that relative resources are made absolute. +// +// In general, it is expected that URI implementations follow IETF RFC3986. +// Implementations are highly recommended to utilize [net/url] to implement URI +// parsing methods, especially [net/url/url.Scheme], [net/url/url.Authority], +// [net/url/url.Path], [net/url/url.Query], and [net/url/url.Fragment]. +type URI interface { + fmt.Stringer + + // Extension should return the file extension of the resource + // (including the dot) referenced by the URI. For example, the + // Extension() of 'file://foo/bar.baz' is '.baz'. May return an + // empty string if the referenced resource has none. + Extension() string + + // Name should return the base name of the item referenced by the URI. + // For example, the name of 'file://foo/bar.baz' is 'bar.baz'. + Name() string + + // MimeType should return the content type of the resource referenced + // by the URI. The returned string should be in the format described + // by Section 5 of RFC2045 ("Content-Type Header Field"). + MimeType() string + + // Scheme should return the URI scheme of the URI as defined by IETF + // RFC3986. For example, the Scheme() of 'file://foo/bar.baz` is + // 'file'. + // + // Scheme should always return the scheme in all lower-case characters. + Scheme() string + + // Authority should return the URI authority, as defined by IETF + // RFC3986. + // + // NOTE: the RFC3986 can be obtained by combining the [net/url.URL.User] + // and [net/url.URL.Host]. Consult IETF RFC3986, section + // 3.2, pp. 17. + // + // Since: 2.0 + Authority() string + + // Path should return the URI path, as defined by IETF RFC3986. + // + // Since: 2.0 + Path() string + + // Query should return the URI query, as defined by IETF RFC3986. + // + // Since: 2.0 + Query() string + + // Fragment should return the URI fragment, as defined by IETF + // RFC3986. + // + // Since: 2.0 + Fragment() string +} + +// ListableURI represents a [URI] that can have child items, most commonly a +// directory on disk in the native filesystem. +// +// Since: 1.4 +type ListableURI interface { + URI + + // List returns a list of child URIs of this URI. + List() ([]URI, error) +} + +// URIWithIcon describes a [URI] that should be rendered with a certain icon in file browsers. +// +// Since: 2.5 +type URIWithIcon interface { + URI + + Icon() Resource +} diff --git a/vendor/fyne.io/fyne/v2/validation.go b/vendor/fyne.io/fyne/v2/validation.go new file mode 100644 index 0000000..10a0cf6 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/validation.go @@ -0,0 +1,17 @@ +package fyne + +// Validatable is an interface for specifying if a widget is validatable. +// +// Since: 1.4 +type Validatable interface { + Validate() error + + // SetOnValidationChanged is used to set the callback that will be triggered when the validation state changes. + // The function might be overwritten by a parent that cares about child validation (e.g. widget.Form). + SetOnValidationChanged(func(error)) +} + +// StringValidator is a function signature for validating string inputs. +// +// Since: 1.4 +type StringValidator func(string) error diff --git a/vendor/fyne.io/fyne/v2/widget.go b/vendor/fyne.io/fyne/v2/widget.go new file mode 100644 index 0000000..644a7a4 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget.go @@ -0,0 +1,33 @@ +package fyne + +// Widget defines the standard behaviours of any widget. This extends +// [CanvasObject]. A widget behaves in the same basic way but will encapsulate +// many child objects to create the rendered widget. +type Widget interface { + CanvasObject + + // CreateRenderer returns a new [WidgetRenderer] for this widget. + // This should not be called by regular code, it is used internally to render a widget. + CreateRenderer() WidgetRenderer +} + +// WidgetRenderer defines the behaviour of a widget's implementation. +// This is returned from a widget's declarative object through [Widget.CreateRenderer] +// and should be exactly one instance per widget in memory. +type WidgetRenderer interface { + // Destroy is a hook that is called when the renderer is being destroyed. + // This happens at some time after the widget is no longer visible, and + // once destroyed, a renderer will not be reused. + // Renderers should dispose and clean up any related resources, if necessary. + Destroy() + // Layout is a hook that is called if the widget needs to be laid out. + // This should never call [Refresh]. + Layout(Size) + // MinSize returns the minimum size of the widget that is rendered by this renderer. + MinSize() Size + // Objects returns all objects that should be drawn. + Objects() []CanvasObject + // Refresh is a hook that is called if the widget has updated and needs to be redrawn. + // This might trigger a [Layout]. + Refresh() +} diff --git a/vendor/fyne.io/fyne/v2/widget/accordion.go b/vendor/fyne.io/fyne/v2/widget/accordion.go new file mode 100644 index 0000000..dd67fb7 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/accordion.go @@ -0,0 +1,307 @@ +package widget + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/internal/widget" + "fyne.io/fyne/v2/theme" +) + +var _ fyne.Widget = (*Accordion)(nil) + +// Accordion displays a list of AccordionItems. +// Each item is represented by a button that reveals a detailed view when tapped. +type Accordion struct { + BaseWidget + Items []*AccordionItem + MultiOpen bool +} + +// NewAccordion creates a new accordion widget. +func NewAccordion(items ...*AccordionItem) *Accordion { + a := &Accordion{ + Items: items, + } + a.ExtendBaseWidget(a) + return a +} + +// Append adds the given item to this Accordion. +func (a *Accordion) Append(item *AccordionItem) { + a.Items = append(a.Items, item) + + a.Refresh() +} + +// Close collapses the item at the given index. +func (a *Accordion) Close(index int) { + if index < 0 || index >= len(a.Items) { + return + } + a.Items[index].Open = false + + a.Refresh() +} + +// CloseAll collapses all items. +func (a *Accordion) CloseAll() { + for _, i := range a.Items { + i.Open = false + } + + a.Refresh() +} + +// CreateRenderer is a private method to Fyne which links this widget to its renderer +func (a *Accordion) CreateRenderer() fyne.WidgetRenderer { + a.ExtendBaseWidget(a) + r := &accordionRenderer{container: a} + r.updateObjects() + return r +} + +// MinSize returns the size that this widget should not shrink below. +func (a *Accordion) MinSize() fyne.Size { + a.ExtendBaseWidget(a) + return a.BaseWidget.MinSize() +} + +// Open expands the item at the given index. +func (a *Accordion) Open(index int) { + if index < 0 || index >= len(a.Items) { + return + } + + for i, ai := range a.Items { + if i == index { + ai.Open = true + } else if !a.MultiOpen { + ai.Open = false + } + } + + a.Refresh() +} + +// OpenAll expands all items, note that your Accordion should have [MultiOpen] set to `true` for this to operate as +// expected. For single-open accordions it will open only the first item. +func (a *Accordion) OpenAll() { + if !a.MultiOpen { + a.Open(0) + return + } + + for _, i := range a.Items { + i.Open = true + } + + a.Refresh() +} + +// Prepend adds the given item to the beginning of this Accordion. +// +// Since: 2.6 +func (a *Accordion) Prepend(item *AccordionItem) { + a.Items = append([]*AccordionItem{item}, a.Items...) + + a.Refresh() +} + +// Remove deletes the given item from this Accordion. +func (a *Accordion) Remove(item *AccordionItem) { + for i, ai := range a.Items { + if ai == item { + a.Items = append(a.Items[:i], a.Items[i+1:]...) + return + } + } +} + +// RemoveIndex deletes the item at the given index from this Accordion. +func (a *Accordion) RemoveIndex(index int) { + if index < 0 || index >= len(a.Items) { + return + } + a.Items = append(a.Items[:index], a.Items[index+1:]...) + + a.Refresh() +} + +type accordionRenderer struct { + widget.BaseRenderer + container *Accordion + headers []*Button + dividers []fyne.CanvasObject +} + +func (r *accordionRenderer) Layout(size fyne.Size) { + th := r.container.Theme() + pad := th.Size(theme.SizeNamePadding) + separator := th.Size(theme.SizeNameSeparatorThickness) + dividerOff := (pad + separator) / 2 + x := float32(0) + y := float32(0) + hasOpen := 0 + + for i, ai := range r.container.Items { + h := r.headers[i] + min := h.MinSize().Height + y += min + + if ai.Open { + y += pad + ai.Detail.MinSize().Height + hasOpen++ + } + if i < len(r.container.Items)-1 { + y += pad + } + } + + extra := (size.Height - y) / float32(hasOpen) + if extra < 0 { + extra = 0 + } + y = 0 + for i, ai := range r.container.Items { + if i != 0 { + div := r.dividers[i-1] + if i > 0 { + div.Move(fyne.NewPos(x, y-dividerOff)) + } + div.Resize(fyne.NewSize(size.Width, separator)) + } + + h := r.headers[i] + h.Move(fyne.NewPos(x, y)) + min := h.MinSize().Height + h.Resize(fyne.NewSize(size.Width, min)) + y += min + + if ai.Open { + y += pad + d := ai.Detail + d.Move(fyne.NewPos(x, y)) + + openSize := ai.Detail.MinSize().Height + extra + d.Resize(fyne.NewSize(size.Width, openSize)) + y += openSize + } + if i < len(r.container.Items)-1 { + y += pad + } + } +} + +func (r *accordionRenderer) MinSize() fyne.Size { + th := r.container.Theme() + pad := th.Size(theme.SizeNamePadding) + size := fyne.Size{} + + for i, ai := range r.container.Items { + if i != 0 { + size.Height += pad + } + min := r.headers[i].MinSize() + size.Width = fyne.Max(size.Width, min.Width) + size.Height += min.Height + min = ai.Detail.MinSize() + size.Width = fyne.Max(size.Width, min.Width) + if ai.Open { + size.Height += min.Height + size.Height += pad + } + } + + return size +} + +func (r *accordionRenderer) Refresh() { + r.updateObjects() + r.Layout(r.container.Size()) + canvas.Refresh(r.container) +} + +func (r *accordionRenderer) updateObjects() { + th := r.container.Theme() + is := len(r.container.Items) + hs := len(r.headers) + ds := len(r.dividers) + i := 0 + for ; i < is; i++ { + ai := r.container.Items[i] + var h *Button + if i < hs { + h = r.headers[i] + h.Show() + } else { + h = &Button{} + r.headers = append(r.headers, h) + hs++ + } + h.Alignment = ButtonAlignLeading + h.IconPlacement = ButtonIconLeadingText + h.Hidden = false + h.Importance = LowImportance + h.Text = ai.Title + index := i // capture + h.OnTapped = func() { + if ai.Open { + r.container.Close(index) + } else { + r.container.Open(index) + } + } + if ai.Open { + h.Icon = th.Icon(theme.IconNameArrowDropUp) + ai.Detail.Show() + } else { + h.Icon = th.Icon(theme.IconNameArrowDropDown) + ai.Detail.Hide() + } + h.Refresh() + } + // Hide extras + for ; i < hs; i++ { + r.headers[i].Hide() + } + // Set objects + objects := make([]fyne.CanvasObject, hs+is+ds) + for i, header := range r.headers { + objects[i] = header + } + for i, item := range r.container.Items { + objects[hs+i] = item.Detail + } + // add dividers + for i = 0; i < ds; i++ { + if i < len(r.container.Items)-1 { + r.dividers[i].Show() + } else { + r.dividers[i].Hide() + } + objects[hs+is+i] = r.dividers[i] + } + // make new dividers + for ; i < is-1; i++ { + div := NewSeparator() + r.dividers = append(r.dividers, div) + objects = append(objects, div) + } + + r.SetObjects(objects) +} + +// AccordionItem represents a single item in an Acc rdion. +type AccordionItem struct { + Title string + Detail fyne.CanvasObject + Open bool +} + +// NewAccordionItem creates a new item for an Accordion. +func NewAccordionItem(title string, detail fyne.CanvasObject) *AccordionItem { + return &AccordionItem{ + Title: title, + Detail: detail, + } +} diff --git a/vendor/fyne.io/fyne/v2/widget/activity.go b/vendor/fyne.io/fyne/v2/widget/activity.go new file mode 100644 index 0000000..91074e2 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/activity.go @@ -0,0 +1,177 @@ +package widget + +import ( + "image/color" + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/theme" +) + +var _ fyne.Widget = (*Activity)(nil) + +// Activity is used to indicate that something is happening that should be waited for, +// or is in the background (depending on usage). +// +// Since: 2.5 +type Activity struct { + BaseWidget + + started bool +} + +// NewActivity returns a widget for indicating activity +// +// Since: 2.5 +func NewActivity() *Activity { + a := &Activity{} + a.ExtendBaseWidget(a) + return a +} + +func (a *Activity) MinSize() fyne.Size { + a.ExtendBaseWidget(a) + return a.BaseWidget.MinSize() +} + +// Start the activity indicator animation +func (a *Activity) Start() { + if a.started { + return // already started + } + + a.started = true + + a.Refresh() +} + +// Stop the activity indicator animation +func (a *Activity) Stop() { + if !a.started { + return // already stopped + } + + a.started = false + + a.Refresh() +} + +func (a *Activity) CreateRenderer() fyne.WidgetRenderer { + dots := make([]fyne.CanvasObject, 3) + v := fyne.CurrentApp().Settings().ThemeVariant() + for i := range dots { + dots[i] = canvas.NewCircle(a.Theme().Color(theme.ColorNameForeground, v)) + } + r := &activityRenderer{dots: dots, parent: a} + r.anim = &fyne.Animation{ + Duration: time.Second * 2, + RepeatCount: fyne.AnimationRepeatForever, + Tick: r.animate, + } + r.updateColor() + + if a.started { + r.start() + } + + return r +} + +var _ fyne.WidgetRenderer = (*activityRenderer)(nil) + +type activityRenderer struct { + anim *fyne.Animation + dots []fyne.CanvasObject + parent *Activity + + bound fyne.Size + maxCol color.NRGBA + maxRad float32 + wasStarted bool +} + +func (a *activityRenderer) Destroy() { + a.parent.started = false + a.stop() +} + +func (a *activityRenderer) Layout(size fyne.Size) { + a.maxRad = fyne.Min(size.Width, size.Height) / 2 + a.bound = size +} + +func (a *activityRenderer) MinSize() fyne.Size { + return fyne.NewSquareSize(a.parent.Theme().Size(theme.SizeNameInlineIcon)) +} + +func (a *activityRenderer) Objects() []fyne.CanvasObject { + return a.dots +} + +func (a *activityRenderer) Refresh() { + if a.parent.started { + if !a.wasStarted { + a.start() + } + } else if a.wasStarted { + a.stop() + } + + a.updateColor() +} + +func (a *activityRenderer) animate(done float32) { + off := done * 2 + if off > 1 { + off = 2 - off + } + + off1 := (done + 0.25) * 2 + if done >= 0.75 { + off1 = (done - 0.75) * 2 + } + if off1 > 1 { + off1 = 2 - off1 + } + + off2 := (done + 0.75) * 2 + if done >= 0.25 { + off2 = (done - 0.25) * 2 + } + if off2 > 1 { + off2 = 2 - off2 + } + + a.scaleDot(a.dots[0].(*canvas.Circle), off) + a.scaleDot(a.dots[1].(*canvas.Circle), off1) + a.scaleDot(a.dots[2].(*canvas.Circle), off2) +} + +func (a *activityRenderer) scaleDot(dot *canvas.Circle, off float32) { + rad := a.maxRad - a.maxRad*off/1.2 + mid := fyne.NewPos(a.bound.Width/2, a.bound.Height/2) + + dot.Move(mid.Subtract(fyne.NewSquareOffsetPos(rad))) + dot.Resize(fyne.NewSquareSize(rad * 2)) + + alpha := uint8(0 + int(float32(a.maxCol.A)*off)) + dot.FillColor = color.NRGBA{R: a.maxCol.R, G: a.maxCol.G, B: a.maxCol.B, A: alpha} + dot.Refresh() +} + +func (a *activityRenderer) start() { + a.wasStarted = true + a.anim.Start() +} + +func (a *activityRenderer) stop() { + a.wasStarted = false + a.anim.Stop() +} + +func (a *activityRenderer) updateColor() { + v := fyne.CurrentApp().Settings().ThemeVariant() + rr, gg, bb, aa := a.parent.Theme().Color(theme.ColorNameForeground, v).RGBA() + a.maxCol = color.NRGBA{R: uint8(rr >> 8), G: uint8(gg >> 8), B: uint8(bb >> 8), A: uint8(aa >> 8)} +} diff --git a/vendor/fyne.io/fyne/v2/widget/bind_helper.go b/vendor/fyne.io/fyne/v2/widget/bind_helper.go new file mode 100644 index 0000000..4d2d465 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/bind_helper.go @@ -0,0 +1,77 @@ +package widget + +import ( + "sync" + "sync/atomic" + + "fyne.io/fyne/v2/data/binding" +) + +// basicBinder stores a DataItem and a function to be called when it changes. +// It provides a convenient way to replace data and callback independently. +type basicBinder struct { + callback atomic.Pointer[func(binding.DataItem)] + + dataListenerPairLock sync.RWMutex + dataListenerPair annotatedListener // access guarded by dataListenerPairLock +} + +// Bind replaces the data item whose changes are tracked by the callback function. +func (binder *basicBinder) Bind(data binding.DataItem) { + listener := binding.NewDataListener(func() { // NB: listener captures `data` but always calls the up-to-date callback + f := binder.callback.Load() + if f == nil || *f == nil { + return + } + + (*f)(data) + }) + data.AddListener(listener) + listenerInfo := annotatedListener{ + data: data, + listener: listener, + } + + binder.dataListenerPairLock.Lock() + binder.unbindLocked() + binder.dataListenerPair = listenerInfo + binder.dataListenerPairLock.Unlock() +} + +// CallWithData passes the currently bound data item as an argument to the +// provided function. +func (binder *basicBinder) CallWithData(f func(data binding.DataItem)) { + binder.dataListenerPairLock.RLock() + data := binder.dataListenerPair.data + binder.dataListenerPairLock.RUnlock() + f(data) +} + +// SetCallback replaces the function to be called when the data changes. +func (binder *basicBinder) SetCallback(f func(data binding.DataItem)) { + binder.callback.Store(&f) +} + +// Unbind requests the callback to be no longer called when the previously bound +// data item changes. +func (binder *basicBinder) Unbind() { + binder.dataListenerPairLock.Lock() + binder.unbindLocked() + binder.dataListenerPairLock.Unlock() +} + +// unbindLocked expects the caller to hold dataListenerPairLock. +func (binder *basicBinder) unbindLocked() { + previousListener := binder.dataListenerPair + binder.dataListenerPair = annotatedListener{nil, nil} + + if previousListener.listener == nil || previousListener.data == nil { + return + } + previousListener.data.RemoveListener(previousListener.listener) +} + +type annotatedListener struct { + data binding.DataItem + listener binding.DataListener +} diff --git a/vendor/fyne.io/fyne/v2/widget/button.go b/vendor/fyne.io/fyne/v2/widget/button.go new file mode 100644 index 0000000..f7ef278 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/button.go @@ -0,0 +1,455 @@ +package widget + +import ( + "image/color" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/driver/desktop" + col "fyne.io/fyne/v2/internal/color" + "fyne.io/fyne/v2/internal/svg" + "fyne.io/fyne/v2/internal/widget" + "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/theme" +) + +// ButtonAlign represents the horizontal alignment of a button. +type ButtonAlign int + +// ButtonIconPlacement represents the ordering of icon & text within a button. +type ButtonIconPlacement int + +// ButtonImportance represents how prominent the button should appear +// +// Since: 1.4 +// +// Deprecated: Use widget.Importance instead +type ButtonImportance = Importance + +// ButtonStyle determines the behaviour and rendering of a button. +type ButtonStyle int + +const ( + // ButtonAlignCenter aligns the icon and the text centrally. + ButtonAlignCenter ButtonAlign = iota + // ButtonAlignLeading aligns the icon and the text with the leading edge. + ButtonAlignLeading + // ButtonAlignTrailing aligns the icon and the text with the trailing edge. + ButtonAlignTrailing +) + +const ( + // ButtonIconLeadingText aligns the icon on the leading edge of the text. + ButtonIconLeadingText ButtonIconPlacement = iota + // ButtonIconTrailingText aligns the icon on the trailing edge of the text. + ButtonIconTrailingText +) + +var _ fyne.Focusable = (*Button)(nil) + +// Button widget has a text label and triggers an event func when clicked +type Button struct { + DisableableWidget + Text string + Icon fyne.Resource + // Specify how prominent the button should be, High will highlight the button and Low will remove some decoration. + // + // Since: 1.4 + Importance Importance + Alignment ButtonAlign + IconPlacement ButtonIconPlacement + + OnTapped func() `json:"-"` + + hovered, focused bool + tapAnim *fyne.Animation +} + +// NewButton creates a new button widget with the set label and tap handler +func NewButton(label string, tapped func()) *Button { + button := &Button{ + Text: label, + OnTapped: tapped, + } + + button.ExtendBaseWidget(button) + return button +} + +// NewButtonWithIcon creates a new button widget with the specified label, themed icon and tap handler +func NewButtonWithIcon(label string, icon fyne.Resource, tapped func()) *Button { + button := &Button{ + Text: label, + Icon: icon, + OnTapped: tapped, + } + + button.ExtendBaseWidget(button) + return button +} + +// CreateRenderer is a private method to Fyne which links this widget to its renderer +func (b *Button) CreateRenderer() fyne.WidgetRenderer { + b.ExtendBaseWidget(b) + th := b.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + seg := &TextSegment{Text: b.Text, Style: RichTextStyleStrong} + seg.Style.Alignment = fyne.TextAlignCenter + text := NewRichText(seg) + text.inset = fyne.NewSquareSize(th.Size(theme.SizeNameInnerPadding)) + + background := canvas.NewRectangle(th.Color(theme.ColorNameButton, v)) + background.CornerRadius = th.Size(theme.SizeNameInputRadius) + tapBG := canvas.NewRectangle(color.Transparent) + b.tapAnim = newButtonTapAnimation(tapBG, b, th) + b.tapAnim.Curve = fyne.AnimationEaseOut + objects := []fyne.CanvasObject{ + background, + tapBG, + text, + } + r := &buttonRenderer{ + BaseRenderer: widget.NewBaseRenderer(objects), + background: background, + tapBG: tapBG, + button: b, + label: text, + layout: layout.NewHBoxLayout(), + } + r.updateIconAndText() + r.applyTheme() + return r +} + +// Cursor returns the cursor type of this widget +func (b *Button) Cursor() desktop.Cursor { + return desktop.DefaultCursor +} + +// FocusGained is a hook called by the focus handling logic after this object gained the focus. +func (b *Button) FocusGained() { + b.focused = true + b.Refresh() +} + +// FocusLost is a hook called by the focus handling logic after this object lost the focus. +func (b *Button) FocusLost() { + b.focused = false + b.Refresh() +} + +// MinSize returns the size that this widget should not shrink below +func (b *Button) MinSize() fyne.Size { + b.ExtendBaseWidget(b) + return b.BaseWidget.MinSize() +} + +// MouseIn is called when a desktop pointer enters the widget +func (b *Button) MouseIn(*desktop.MouseEvent) { + b.hovered = true + b.Refresh() +} + +// MouseMoved is called when a desktop pointer hovers over the widget +func (b *Button) MouseMoved(*desktop.MouseEvent) { +} + +// MouseOut is called when a desktop pointer exits the widget +func (b *Button) MouseOut() { + b.hovered = false + b.Refresh() +} + +// SetIcon updates the icon on a label - pass nil to hide an icon +func (b *Button) SetIcon(icon fyne.Resource) { + b.Icon = icon + + b.Refresh() +} + +// SetText allows the button label to be changed +func (b *Button) SetText(text string) { + b.Text = text + + b.Refresh() +} + +// Tapped is called when a pointer tapped event is captured and triggers any tap handler +func (b *Button) Tapped(*fyne.PointEvent) { + if b.Disabled() { + return + } + + b.tapAnimation() + + if onTapped := b.OnTapped; onTapped != nil { + onTapped() + } +} + +// TypedRune is a hook called by the input handling logic on text input events if this object is focused. +func (b *Button) TypedRune(rune) { +} + +// TypedKey is a hook called by the input handling logic on key events if this object is focused. +func (b *Button) TypedKey(ev *fyne.KeyEvent) { + if ev.Name == fyne.KeySpace { + b.Tapped(nil) + } +} + +func (b *Button) tapAnimation() { + if b.tapAnim == nil { + return + } + b.tapAnim.Stop() + + if fyne.CurrentApp().Settings().ShowAnimations() { + b.tapAnim.Start() + } +} + +type buttonRenderer struct { + widget.BaseRenderer + + icon *canvas.Image + label *RichText + background *canvas.Rectangle + tapBG *canvas.Rectangle + button *Button + layout fyne.Layout +} + +// Layout the components of the button widget +func (r *buttonRenderer) Layout(size fyne.Size) { + r.background.Resize(size) + r.tapBG.Resize(size) + + th := r.button.Theme() + padding := r.padding(th) + hasIcon := r.icon != nil + hasLabel := r.label.Segments[0].(*TextSegment).Text != "" + if !hasIcon && !hasLabel { + // Nothing to layout + return + } + iconSize := fyne.NewSquareSize(th.Size(theme.SizeNameInlineIcon)) + labelSize := r.label.MinSize() + + if hasLabel { + if hasIcon { + // Both + var objects []fyne.CanvasObject + if r.button.IconPlacement == ButtonIconLeadingText { + objects = append(objects, r.icon, r.label) + } else { + objects = append(objects, r.label, r.icon) + } + r.icon.SetMinSize(iconSize) + min := r.layout.MinSize(objects) + r.layout.Layout(objects, min) + pos := alignedPosition(r.button.Alignment, padding, min, size) + labelOff := (min.Height - labelSize.Height) / 2 + r.label.Move(r.label.Position().Add(pos).AddXY(0, labelOff)) + r.icon.Move(r.icon.Position().Add(pos)) + } else { + // Label Only + r.label.Move(alignedPosition(r.button.Alignment, padding, labelSize, size)) + r.label.Resize(labelSize) + } + } else { + // Icon Only + r.icon.Move(alignedPosition(r.button.Alignment, padding, iconSize, size)) + r.icon.Resize(iconSize) + } +} + +// MinSize calculates the minimum size of a button. +// This is based on the contained text, any icon that is set and a standard +// amount of padding added. +func (r *buttonRenderer) MinSize() (size fyne.Size) { + th := r.button.Theme() + hasIcon := r.icon != nil + hasLabel := r.label.Segments[0].(*TextSegment).Text != "" + iconSize := fyne.NewSquareSize(th.Size(theme.SizeNameInlineIcon)) + labelSize := r.label.MinSize() + if hasLabel { + size.Width = labelSize.Width + } + if hasIcon { + if hasLabel { + size.Width += th.Size(theme.SizeNamePadding) + } + size.Width += iconSize.Width + } + size.Height = fyne.Max(labelSize.Height, iconSize.Height) + size = size.Add(r.padding(th)) + return size +} + +func (r *buttonRenderer) Refresh() { + th := r.button.Theme() + r.label.inset = fyne.NewSquareSize(th.Size(theme.SizeNameInnerPadding)) + + r.label.Segments[0].(*TextSegment).Text = r.button.Text + r.updateIconAndText() + r.applyTheme() + + r.background.Refresh() + r.Layout(r.button.Size()) + canvas.Refresh(r.button.super()) +} + +// applyTheme updates this button to match the current theme +// must be called with the button propertyLock RLocked +func (r *buttonRenderer) applyTheme() { + th := r.button.Theme() + fgColorName, bgColorName, bgBlendName := r.buttonColorNames() + if bg := r.background; bg != nil { + v := fyne.CurrentApp().Settings().ThemeVariant() + bgColor := color.Color(color.Transparent) + if bgColorName != "" { + bgColor = th.Color(bgColorName, v) + } + if bgBlendName != "" { + bgColor = blendColor(bgColor, th.Color(bgBlendName, v)) + } + bg.FillColor = bgColor + bg.CornerRadius = th.Size(theme.SizeNameInputRadius) + bg.Refresh() + } + + r.label.Segments[0].(*TextSegment).Style.ColorName = fgColorName + r.label.Refresh() + if r.icon != nil && r.icon.Resource != nil { + icon := r.icon.Resource + if r.button.Importance != MediumImportance && r.button.Importance != LowImportance { + if thRes, ok := icon.(fyne.ThemedResource); ok { + if thRes.ThemeColorName() != fgColorName { + icon = theme.NewColoredResource(icon, fgColorName) + } + } + } + r.icon.Resource = icon + r.icon.Refresh() + } +} + +func (r *buttonRenderer) buttonColorNames() (foreground, background, backgroundBlend fyne.ThemeColorName) { + foreground = theme.ColorNameForeground + b := r.button + if b.Disabled() { + foreground = theme.ColorNameDisabled + if b.Importance != LowImportance { + background = theme.ColorNameDisabledButton + } + } else if b.focused { + backgroundBlend = theme.ColorNameFocus + } else if b.hovered { + backgroundBlend = theme.ColorNameHover + } + if background == "" { + switch b.Importance { + case DangerImportance: + foreground = theme.ColorNameForegroundOnError + background = theme.ColorNameError + case HighImportance: + foreground = theme.ColorNameForegroundOnPrimary + background = theme.ColorNamePrimary + case LowImportance: + if backgroundBlend != "" { + background = theme.ColorNameButton + } + case SuccessImportance: + foreground = theme.ColorNameForegroundOnSuccess + background = theme.ColorNameSuccess + case WarningImportance: + foreground = theme.ColorNameForegroundOnWarning + background = theme.ColorNameWarning + default: + background = theme.ColorNameButton + } + } + return foreground, background, backgroundBlend +} + +func (r *buttonRenderer) padding(th fyne.Theme) fyne.Size { + return fyne.NewSquareSize(th.Size(theme.SizeNameInnerPadding) * 2) +} + +// must be called with r.button.propertyLock RLocked +func (r *buttonRenderer) updateIconAndText() { + if r.button.Icon != nil && !r.button.Hidden { + icon := r.button.Icon + if r.icon == nil { + r.icon = canvas.NewImageFromResource(icon) + r.icon.FillMode = canvas.ImageFillContain + r.SetObjects([]fyne.CanvasObject{r.background, r.tapBG, r.label, r.icon}) + } + // TODO support disabling bitmap resource not just SVG + if r.button.Disabled() && svg.IsResourceSVG(icon) { + icon = theme.NewDisabledResource(icon) + } + r.icon.Resource = icon + r.icon.Refresh() + r.icon.Show() + } else if r.icon != nil { + r.icon.Hide() + } + if r.button.Text == "" { + r.label.Hide() + } else { + r.label.Show() + } + r.label.Refresh() +} + +func alignedPosition(align ButtonAlign, padding, objectSize, layoutSize fyne.Size) (pos fyne.Position) { + pos.Y = (layoutSize.Height - objectSize.Height) / 2 + switch align { + case ButtonAlignCenter: + pos.X = (layoutSize.Width - objectSize.Width) / 2 + case ButtonAlignLeading: + pos.X = padding.Width / 2 + case ButtonAlignTrailing: + pos.X = layoutSize.Width - objectSize.Width - padding.Width/2 + } + return pos +} + +func blendColor(under, over color.Color) color.Color { + // This alpha blends with the over operator, and accounts for RGBA() returning alpha-premultiplied values + dstR, dstG, dstB, dstA := under.RGBA() + srcR, srcG, srcB, srcA := over.RGBA() + + srcAlpha := float32(srcA) / 0xFFFF + dstAlpha := float32(dstA) / 0xFFFF + + outAlpha := srcAlpha + dstAlpha*(1-srcAlpha) + outR := srcR + uint32(float32(dstR)*(1-srcAlpha)) + outG := srcG + uint32(float32(dstG)*(1-srcAlpha)) + outB := srcB + uint32(float32(dstB)*(1-srcAlpha)) + // We create an RGBA64 here because the color components are already alpha-premultiplied 16-bit values (they're just stored in uint32s). + return color.RGBA64{R: uint16(outR), G: uint16(outG), B: uint16(outB), A: uint16(outAlpha * 0xFFFF)} +} + +func newButtonTapAnimation(bg *canvas.Rectangle, w fyne.Widget, th fyne.Theme) *fyne.Animation { + v := fyne.CurrentApp().Settings().ThemeVariant() + return fyne.NewAnimation(canvas.DurationStandard, func(done float32) { + mid := w.Size().Width / 2 + size := mid * done + bg.Resize(fyne.NewSize(size*2, w.Size().Height)) + bg.Move(fyne.NewPos(mid-size, 0)) + + r, g, bb, a := col.ToNRGBA(th.Color(theme.ColorNamePressed, v)) + aa := uint8(a) + fade := aa - uint8(float32(aa)*done) + if fade > 0 { + bg.FillColor = &color.NRGBA{R: uint8(r), G: uint8(g), B: uint8(bb), A: fade} + } else { + bg.FillColor = color.Transparent + } + canvas.Refresh(bg) + }) +} diff --git a/vendor/fyne.io/fyne/v2/widget/calendar.go b/vendor/fyne.io/fyne/v2/widget/calendar.go new file mode 100644 index 0000000..41a19c1 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/calendar.go @@ -0,0 +1,240 @@ +package widget + +import ( + "math" + "strconv" + "strings" + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/lang" + "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/theme" +) + +// Declare conformity with Layout interface +var _ fyne.Layout = (*calendarLayout)(nil) + +const ( + daysPerWeek = 7 + maxWeeksPerMonth = 6 +) + +var minCellContent = NewLabel("22") + +// Calendar creates a new date time picker which returns a time object +// +// Since: 2.6 +type Calendar struct { + BaseWidget + currentTime time.Time + + monthPrevious *Button + monthNext *Button + monthLabel *Label + + dates *fyne.Container + + OnChanged func(time.Time) `json:"-"` +} + +// NewCalendar creates a calendar instance +// +// Since: 2.6 +func NewCalendar(cT time.Time, changed func(time.Time)) *Calendar { + c := &Calendar{ + currentTime: cT, + OnChanged: changed, + } + + c.ExtendBaseWidget(c) + return c +} + +// CreateRenderer returns a new WidgetRenderer for this widget. +// This should not be called by regular code, it is used internally to render a widget. +func (c *Calendar) CreateRenderer() fyne.WidgetRenderer { + c.monthPrevious = NewButtonWithIcon("", theme.NavigateBackIcon(), func() { + c.currentTime = c.currentTime.AddDate(0, -1, 0) + // Dates are 'normalised', forcing date to start from the start of the month ensures move from March to February + c.currentTime = time.Date(c.currentTime.Year(), c.currentTime.Month(), 1, 0, 0, 0, 0, c.currentTime.Location()) + c.monthLabel.SetText(c.monthYear()) + c.dates.Objects = c.calendarObjects() + }) + c.monthPrevious.Importance = LowImportance + + c.monthNext = NewButtonWithIcon("", theme.NavigateNextIcon(), func() { + c.currentTime = c.currentTime.AddDate(0, 1, 0) + c.monthLabel.SetText(c.monthYear()) + c.dates.Objects = c.calendarObjects() + }) + c.monthNext.Importance = LowImportance + + c.monthLabel = NewLabel(c.monthYear()) + + nav := &fyne.Container{ + Layout: layout.NewBorderLayout(nil, nil, c.monthPrevious, c.monthNext), + Objects: []fyne.CanvasObject{ + c.monthPrevious, c.monthNext, + &fyne.Container{Layout: layout.NewCenterLayout(), Objects: []fyne.CanvasObject{c.monthLabel}}, + }, + } + + c.dates = &fyne.Container{Layout: newCalendarLayout(), Objects: c.calendarObjects()} + + dateContainer := &fyne.Container{ + Layout: layout.NewBorderLayout(nav, nil, nil, nil), + Objects: []fyne.CanvasObject{nav, c.dates}, + } + + return NewSimpleRenderer(dateContainer) +} + +func (c *Calendar) calendarObjects() []fyne.CanvasObject { + offset := 0 + switch getLocaleWeekStart() { + case "Saturday": + offset = 6 + case "Sunday": + default: + offset = 1 + } + + var columnHeadings []fyne.CanvasObject + for i := 0; i < daysPerWeek; i++ { + t := NewLabel(shortDayName(time.Weekday((i + offset) % daysPerWeek).String())) + t.Alignment = fyne.TextAlignCenter + columnHeadings = append(columnHeadings, t) + } + return append(columnHeadings, c.daysOfMonth()...) +} + +func (c *Calendar) dateForButton(dayNum int) time.Time { + oldName, off := c.currentTime.Zone() + return time.Date(c.currentTime.Year(), c.currentTime.Month(), dayNum, c.currentTime.Hour(), c.currentTime.Minute(), 0, 0, time.FixedZone(oldName, off)).In(c.currentTime.Location()) +} + +func (c *Calendar) daysOfMonth() []fyne.CanvasObject { + start := time.Date(c.currentTime.Year(), c.currentTime.Month(), 1, 0, 0, 0, 0, c.currentTime.Location()) + var buttons []fyne.CanvasObject + + dayIndex := int(start.Weekday()) + // account for Go time pkg starting on sunday at index 0 + switch getLocaleWeekStart() { + case "Saturday": + if dayIndex == daysPerWeek-1 { + dayIndex = 0 + } else { + dayIndex++ + } + case "Sunday": // nothing to do + default: + if dayIndex == 0 { + dayIndex += daysPerWeek - 1 + } else { + dayIndex-- + } + } + + // add spacers if week doesn't start on Monday + for i := 0; i < dayIndex; i++ { + buttons = append(buttons, layout.NewSpacer()) + } + + for d := start; d.Month() == start.Month(); d = d.AddDate(0, 0, 1) { + dayNum := d.Day() + s := strconv.Itoa(dayNum) + b := NewButton(s, func() { + selectedDate := c.dateForButton(dayNum) + + c.OnChanged(selectedDate) + }) + b.Importance = LowImportance + + buttons = append(buttons, b) + } + + return buttons +} + +func (c *Calendar) monthYear() string { + return c.currentTime.Format("January 2006") +} + +type calendarLayout struct { + cellSize fyne.Size +} + +func newCalendarLayout() fyne.Layout { + return &calendarLayout{} +} + +// Layout is called to pack all child objects into a specified size. +// For a calendar grid this will pack objects into a table format. +func (g *calendarLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) { + weeks := 1 + day := 0 + for i, child := range objects { + if !child.Visible() { + continue + } + + if day%daysPerWeek == 0 && i >= daysPerWeek { + weeks++ + } + day++ + } + + g.cellSize = fyne.NewSize(size.Width/float32(daysPerWeek), + size.Height/float32(weeks)) + row, col := 0, 0 + i := 0 + for _, child := range objects { + if !child.Visible() { + continue + } + + lead := g.getLeading(row, col) + trail := g.getTrailing(row, col) + child.Move(lead) + child.Resize(fyne.NewSize(trail.X, trail.Y).Subtract(lead)) + + if (i+1)%daysPerWeek == 0 { + row++ + col = 0 + } else { + col++ + } + i++ + } +} + +// MinSize sets the minimum size for the calendar +func (g *calendarLayout) MinSize(_ []fyne.CanvasObject) fyne.Size { + pad := theme.Padding() + largestMin := minCellContent.MinSize() + return fyne.NewSize(largestMin.Width*daysPerWeek+pad*(daysPerWeek-1), + largestMin.Height*maxWeeksPerMonth+pad*(maxWeeksPerMonth-1)) +} + +// Get the leading edge position of a grid cell. +// The row and col specify where the cell is in the calendar. +func (g *calendarLayout) getLeading(row, col int) fyne.Position { + x := (g.cellSize.Width) * float32(col) + y := (g.cellSize.Height) * float32(row) + + return fyne.NewPos(float32(math.Round(float64(x))), float32(math.Round(float64(y)))) +} + +// Get the trailing edge position of a grid cell. +// The row and col specify where the cell is in the calendar. +func (g *calendarLayout) getTrailing(row, col int) fyne.Position { + return g.getLeading(row+1, col+1) +} + +func shortDayName(in string) string { + lower := strings.ToLower(in) + key := lower + ".short" + long := lang.X(lower, in) + return strings.ToUpper(lang.X(key, long[:3])) +} diff --git a/vendor/fyne.io/fyne/v2/widget/card.go b/vendor/fyne.io/fyne/v2/widget/card.go new file mode 100644 index 0000000..fc0591a --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/card.go @@ -0,0 +1,241 @@ +package widget + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/internal/widget" + "fyne.io/fyne/v2/theme" +) + +// Card widget groups title, subtitle with content and a header image +// +// Since: 1.4 +type Card struct { + BaseWidget + Title, Subtitle string + Image *canvas.Image + Content fyne.CanvasObject +} + +// NewCard creates a new card widget with the specified title, subtitle and content (all optional). +// +// Since: 1.4 +func NewCard(title, subtitle string, content fyne.CanvasObject) *Card { + card := &Card{ + Title: title, + Subtitle: subtitle, + Content: content, + } + + card.ExtendBaseWidget(card) + return card +} + +// CreateRenderer is a private method to Fyne which links this widget to its renderer +func (c *Card) CreateRenderer() fyne.WidgetRenderer { + c.ExtendBaseWidget(c) + th := c.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + header := canvas.NewText(c.Title, th.Color(theme.ColorNameForeground, v)) + header.TextStyle.Bold = true + subHeader := canvas.NewText(c.Subtitle, header.Color) + + objects := []fyne.CanvasObject{header, subHeader} + if c.Image != nil { + objects = append(objects, c.Image) + } + if c.Content != nil { + objects = append(objects, c.Content) + } + r := &cardRenderer{ + widget.NewShadowingRenderer(objects, widget.CardLevel), + header, subHeader, c, + } + r.applyTheme() + return r +} + +// MinSize returns the size that this widget should not shrink below +func (c *Card) MinSize() fyne.Size { + c.ExtendBaseWidget(c) + return c.BaseWidget.MinSize() +} + +// SetContent changes the body of this card to have the specified content. +func (c *Card) SetContent(obj fyne.CanvasObject) { + c.Content = obj + + c.Refresh() +} + +// SetImage changes the image displayed above the title for this card. +func (c *Card) SetImage(img *canvas.Image) { + c.Image = img + + c.Refresh() +} + +// SetSubTitle updates the secondary title for this card. +func (c *Card) SetSubTitle(text string) { + c.Subtitle = text + + c.Refresh() +} + +// SetTitle updates the main title for this card. +func (c *Card) SetTitle(text string) { + c.Title = text + + c.Refresh() +} + +type cardRenderer struct { + *widget.ShadowingRenderer + + header, subHeader *canvas.Text + + card *Card +} + +const ( + cardMediaHeight = 128 +) + +// Layout the components of the card container. +func (c *cardRenderer) Layout(size fyne.Size) { + padding := c.card.Theme().Size(theme.SizeNamePadding) + pos := fyne.NewSquareOffsetPos(padding / 2) + size = size.Subtract(fyne.NewSquareSize(padding)) + c.LayoutShadow(size, pos) + + if c.card.Image != nil { + c.card.Image.Move(pos) + c.card.Image.Resize(fyne.NewSize(size.Width, cardMediaHeight)) + pos.Y += cardMediaHeight + } + + if c.card.Title != "" || c.card.Subtitle != "" { + titlePad := padding * 2 + size.Width -= titlePad * 2 + pos.X += titlePad + pos.Y += titlePad + + if c.card.Title != "" { + height := c.header.MinSize().Height + c.header.Move(pos) + c.header.Resize(fyne.NewSize(size.Width, height)) + pos.Y += height + padding + } + + if c.card.Subtitle != "" { + height := c.subHeader.MinSize().Height + c.subHeader.Move(pos) + c.subHeader.Resize(fyne.NewSize(size.Width, height)) + pos.Y += height + padding + } + + size.Width = size.Width + titlePad*2 + pos.X = pos.X - titlePad + pos.Y += titlePad + } + + size.Width -= padding * 2 + pos.X += padding + if c.card.Content != nil { + height := size.Height - padding*2 - (pos.Y - padding/2) // adjust for content and initial offset + if c.card.Title != "" || c.card.Subtitle != "" { + height += padding + pos.Y -= padding + } + c.card.Content.Move(pos.Add(fyne.NewPos(0, padding))) + c.card.Content.Resize(fyne.NewSize(size.Width, height)) + } +} + +// MinSize calculates the minimum size of a card. +// This is based on the contained text, image and content. +func (c *cardRenderer) MinSize() fyne.Size { + hasHeader := c.card.Title != "" + hasSubHeader := c.card.Subtitle != "" + hasImage := c.card.Image != nil + hasContent := c.card.Content != nil + + padding := c.card.Theme().Size(theme.SizeNamePadding) + if !hasHeader && !hasSubHeader && !hasContent { // just image, or nothing + if c.card.Image == nil { + return fyne.NewSize(padding, padding) // empty, just space for border + } + return fyne.NewSize(c.card.Image.MinSize().Width+padding, cardMediaHeight+padding) + } + + min := fyne.NewSize(padding, padding) + if hasImage { + min = fyne.NewSize(min.Width, min.Height+cardMediaHeight) + } + + if hasHeader || hasSubHeader { + titlePad := padding * 2 + min = min.Add(fyne.NewSize(0, titlePad*2)) + if hasHeader { + headerMin := c.header.MinSize() + min = fyne.NewSize(fyne.Max(min.Width, headerMin.Width+titlePad*2+padding), + min.Height+headerMin.Height) + if hasSubHeader { + min.Height += padding + } + } + if hasSubHeader { + subHeaderMin := c.subHeader.MinSize() + min = fyne.NewSize(fyne.Max(min.Width, subHeaderMin.Width+titlePad*2+padding), + min.Height+subHeaderMin.Height) + } + } + + if hasContent { + contentMin := c.card.Content.MinSize() + min = fyne.NewSize(fyne.Max(min.Width, contentMin.Width+padding*3), + min.Height+contentMin.Height+padding*2) + } + + return min +} + +func (c *cardRenderer) Refresh() { + c.header.Text = c.card.Title + c.header.Refresh() + c.subHeader.Text = c.card.Subtitle + c.subHeader.Refresh() + + objects := []fyne.CanvasObject{c.header, c.subHeader} + if c.card.Image != nil { + objects = append(objects, c.card.Image) + } + if c.card.Content != nil { + objects = append(objects, c.card.Content) + } + c.ShadowingRenderer.SetObjects(objects) + + c.applyTheme() + c.Layout(c.card.Size()) + c.ShadowingRenderer.RefreshShadow() + canvas.Refresh(c.card.super()) +} + +// applyTheme updates this button to match the current theme +func (c *cardRenderer) applyTheme() { + th := c.card.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + if c.header != nil { + c.header.TextSize = th.Size(theme.SizeNameHeadingText) + c.header.Color = th.Color(theme.ColorNameForeground, v) + } + if c.subHeader != nil { + c.subHeader.TextSize = th.Size(theme.SizeNameText) + c.subHeader.Color = th.Color(theme.ColorNameForeground, v) + } + if c.card.Content != nil { + c.card.Content.Refresh() + } +} diff --git a/vendor/fyne.io/fyne/v2/widget/check.go b/vendor/fyne.io/fyne/v2/widget/check.go new file mode 100644 index 0000000..87ad07d --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/check.go @@ -0,0 +1,398 @@ +package widget + +import ( + "fmt" + "image/color" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/data/binding" + "fyne.io/fyne/v2/driver/desktop" + "fyne.io/fyne/v2/internal/widget" + "fyne.io/fyne/v2/theme" +) + +// Check widget has a text label and a checked (or unchecked) icon and triggers an event func when toggled +type Check struct { + DisableableWidget + Text string + Checked bool + + // Partial check is when there is an indeterminate state (usually meaning that child items are some-what checked). + // Turning this on will override the checked state and show a dash icon (neither checked nor unchecked). + // The user interaction cannot turn this on, tapping a partial check state will set `Checked` to true. + // + // Since: 2.6 + Partial bool + + OnChanged func(bool) `json:"-"` + + focused bool + hovered bool + + binder basicBinder + + minSize fyne.Size // cached for hover/tap position calculations +} + +// NewCheck creates a new check widget with the set label and change handler +func NewCheck(label string, changed func(bool)) *Check { + c := &Check{ + Text: label, + OnChanged: changed, + } + + c.ExtendBaseWidget(c) + return c +} + +// NewCheckWithData returns a check widget connected with the specified data source. +// +// Since: 2.0 +func NewCheckWithData(label string, data binding.Bool) *Check { + check := NewCheck(label, nil) + check.Bind(data) + + return check +} + +// Bind connects the specified data source to this Check. +// The current value will be displayed and any changes in the data will cause the widget to update. +// User interactions with this Check will set the value into the data source. +// +// Since: 2.0 +func (c *Check) Bind(data binding.Bool) { + c.binder.SetCallback(c.updateFromData) + c.binder.Bind(data) + + c.OnChanged = func(_ bool) { + c.binder.CallWithData(c.writeData) + } +} + +// SetChecked sets the checked state and refreshes widget +// If the `Partial` state is set this will be turned off to respect the `checked` bool passed in here. +func (c *Check) SetChecked(checked bool) { + if checked == c.Checked && !c.Partial { + return + } + + c.Partial = false + c.Checked = checked + onChanged := c.OnChanged + + if onChanged != nil { + onChanged(checked) + } + + c.Refresh() +} + +// Hide this widget, if it was previously visible +func (c *Check) Hide() { + if c.focused { + c.FocusLost() + impl := c.super() + + if c := fyne.CurrentApp().Driver().CanvasForObject(impl); c != nil { + c.Focus(nil) + } + } + + c.BaseWidget.Hide() +} + +// MouseIn is called when a desktop pointer enters the widget +func (c *Check) MouseIn(me *desktop.MouseEvent) { + c.MouseMoved(me) +} + +// MouseOut is called when a desktop pointer exits the widget +func (c *Check) MouseOut() { + if c.hovered { + c.hovered = false + c.Refresh() + } +} + +// MouseMoved is called when a desktop pointer hovers over the widget +func (c *Check) MouseMoved(me *desktop.MouseEvent) { + if c.Disabled() { + return + } + + oldHovered := c.hovered + + // only hovered if cached minSize has not been initialized (test code) + // or the pointer is within the "active" area of the widget (its minSize) + c.hovered = c.minSize.IsZero() || + (me.Position.X <= c.minSize.Width && me.Position.Y <= c.minSize.Height) + + if oldHovered != c.hovered { + c.Refresh() + } +} + +// Tapped is called when a pointer tapped event is captured and triggers any change handler +func (c *Check) Tapped(pe *fyne.PointEvent) { + if c.Disabled() { + return + } + + minHeight := c.minSize.Height + minY := (c.Size().Height - minHeight) / 2 + if !c.minSize.IsZero() && + (pe.Position.X > c.minSize.Width || pe.Position.Y < minY || pe.Position.Y > minY+minHeight) { + // tapped outside the active area of the widget + return + } + + if !c.focused { + focusIfNotMobile(c.super()) + } + c.SetChecked(!c.Checked) +} + +// MinSize returns the size that this widget should not shrink below +func (c *Check) MinSize() fyne.Size { + c.ExtendBaseWidget(c) + c.minSize = c.BaseWidget.MinSize() + return c.minSize +} + +// CreateRenderer is a private method to Fyne which links this widget to its renderer +func (c *Check) CreateRenderer() fyne.WidgetRenderer { + th := c.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + c.ExtendBaseWidget(c) + bg := canvas.NewImageFromResource(th.Icon(theme.IconNameCheckButtonFill)) + icon := canvas.NewImageFromResource(th.Icon(theme.IconNameCheckButton)) + + text := canvas.NewText(c.Text, th.Color(theme.ColorNameForeground, v)) + text.Alignment = fyne.TextAlignLeading + + focusIndicator := canvas.NewCircle(th.Color(theme.ColorNameBackground, v)) + r := &checkRenderer{ + widget.NewBaseRenderer([]fyne.CanvasObject{focusIndicator, bg, icon, text}), + bg, + icon, + text, + focusIndicator, + c, + } + r.applyTheme(th, v) + r.updateLabel() + r.updateResource(th) + r.updateFocusIndicator(th, v) + return r +} + +// FocusGained is called when the Check has been given focus. +func (c *Check) FocusGained() { + if c.Disabled() { + return + } + c.focused = true + + c.Refresh() +} + +// FocusLost is called when the Check has had focus removed. +func (c *Check) FocusLost() { + c.focused = false + + c.Refresh() +} + +// TypedRune receives text input events when the Check is focused. +func (c *Check) TypedRune(r rune) { + if c.Disabled() { + return + } + if r == ' ' { + c.SetChecked(!c.Checked) + } +} + +// TypedKey receives key input events when the Check is focused. +func (c *Check) TypedKey(key *fyne.KeyEvent) {} + +// SetText sets the text of the Check +// +// Since: 2.4 +func (c *Check) SetText(text string) { + c.Text = text + c.Refresh() +} + +// Unbind disconnects any configured data source from this Check. +// The current value will remain at the last value of the data source. +// +// Since: 2.0 +func (c *Check) Unbind() { + c.OnChanged = nil + c.binder.Unbind() +} + +func (c *Check) updateFromData(data binding.DataItem) { + if data == nil { + return + } + boolSource, ok := data.(binding.Bool) + if !ok { + return + } + val, err := boolSource.Get() + if err != nil { + fyne.LogError("Error getting current data value", err) + return + } + c.SetChecked(val) // if val != c.Checked, this will call updateFromData again, but only once +} + +func (c *Check) writeData(data binding.DataItem) { + if data == nil { + return + } + boolTarget, ok := data.(binding.Bool) + if !ok { + return + } + currentValue, err := boolTarget.Get() + if err != nil { + return + } + if currentValue != c.Checked { + err := boolTarget.Set(c.Checked) + if err != nil { + fyne.LogError(fmt.Sprintf("Failed to set binding value to %t", c.Checked), err) + } + } +} + +type checkRenderer struct { + widget.BaseRenderer + bg, icon *canvas.Image + label *canvas.Text + focusIndicator *canvas.Circle + check *Check +} + +// MinSize calculates the minimum size of a check. +// This is based on the contained text, the check icon and a standard amount of padding added. +func (c *checkRenderer) MinSize() fyne.Size { + th := c.check.Theme() + + pad4 := th.Size(theme.SizeNameInnerPadding) * 2 + min := c.label.MinSize().Add(fyne.NewSize(th.Size(theme.SizeNameInlineIcon)+pad4, pad4)) + + if c.check.Text != "" { + min.Add(fyne.NewSize(th.Size(theme.SizeNamePadding), 0)) + } + + return min +} + +// Layout the components of the check widget +func (c *checkRenderer) Layout(size fyne.Size) { + th := c.check.Theme() + innerPadding := th.Size(theme.SizeNameInnerPadding) + borderSize := th.Size(theme.SizeNameInputBorder) + iconInlineSize := th.Size(theme.SizeNameInlineIcon) + + focusIndicatorSize := fyne.NewSquareSize(iconInlineSize + innerPadding) + c.focusIndicator.Resize(focusIndicatorSize) + c.focusIndicator.Move(fyne.NewPos(borderSize, (size.Height-focusIndicatorSize.Height)/2)) + + xOff := focusIndicatorSize.Width + borderSize*2 + labelSize := size.SubtractWidthHeight(xOff, 0) + c.label.Resize(labelSize) + c.label.Move(fyne.NewPos(xOff, 0)) + + iconPos := fyne.NewPos(innerPadding/2+borderSize, (size.Height-iconInlineSize)/2) + iconSize := fyne.NewSquareSize(iconInlineSize) + c.bg.Move(iconPos) + c.bg.Resize(iconSize) + c.icon.Move(iconPos) + c.icon.Resize(iconSize) +} + +// applyTheme updates this Check to the current theme +func (c *checkRenderer) applyTheme(th fyne.Theme, v fyne.ThemeVariant) { + c.label.Color = th.Color(theme.ColorNameForeground, v) + c.label.TextSize = th.Size(theme.SizeNameText) + if c.check.Disabled() { + c.label.Color = th.Color(theme.ColorNameDisabled, v) + } +} + +func (c *checkRenderer) Refresh() { + th := c.check.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + c.applyTheme(th, v) + c.updateLabel() + c.updateResource(th) + c.updateFocusIndicator(th, v) + canvas.Refresh(c.check.super()) +} + +// must be called while holding c.check.propertyLock for reading +func (c *checkRenderer) updateLabel() { + c.label.Text = c.check.Text +} + +// must be called while holding c.check.propertyLock for reading +func (c *checkRenderer) updateResource(th fyne.Theme) { + res := theme.NewThemedResource(th.Icon(theme.IconNameCheckButton)) + res.ColorName = theme.ColorNameInputBorder + bgRes := theme.NewThemedResource(th.Icon(theme.IconNameCheckButtonFill)) + bgRes.ColorName = theme.ColorNameInputBackground + + if c.check.Partial { + res = theme.NewThemedResource(th.Icon(theme.IconNameCheckButtonPartial)) + res.ColorName = theme.ColorNamePrimary + bgRes.ColorName = theme.ColorNameBackground + } else if c.check.Checked { + res = theme.NewThemedResource(th.Icon(theme.IconNameCheckButtonChecked)) + res.ColorName = theme.ColorNamePrimary + bgRes.ColorName = theme.ColorNameBackground + } + if c.check.Disabled() { + if c.check.Checked { + res = theme.NewThemedResource(theme.CheckButtonCheckedIcon()) + } + res.ColorName = theme.ColorNameDisabled + bgRes.ColorName = theme.ColorNameBackground + } + c.icon.Resource = res + c.icon.Refresh() + c.bg.Resource = bgRes + c.bg.Refresh() +} + +// must be called while holding c.check.propertyLock for reading +func (c *checkRenderer) updateFocusIndicator(th fyne.Theme, v fyne.ThemeVariant) { + if c.check.Disabled() { + c.focusIndicator.FillColor = color.Transparent + } else if c.check.focused { + c.focusIndicator.FillColor = th.Color(theme.ColorNameFocus, v) + } else if c.check.hovered { + c.focusIndicator.FillColor = th.Color(theme.ColorNameHover, v) + } else { + c.focusIndicator.FillColor = color.Transparent + } +} + +func focusIfNotMobile(w fyne.Widget) { + if w == nil { + return + } + + if !fyne.CurrentDevice().IsMobile() { + if c := fyne.CurrentApp().Driver().CanvasForObject(w); c != nil { + c.Focus(w.(fyne.Focusable)) + } + } +} diff --git a/vendor/fyne.io/fyne/v2/widget/check_group.go b/vendor/fyne.io/fyne/v2/widget/check_group.go new file mode 100644 index 0000000..a00c463 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/check_group.go @@ -0,0 +1,275 @@ +package widget + +import ( + "strings" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/internal/widget" +) + +var _ fyne.Widget = (*CheckGroup)(nil) + +// CheckGroup widget has a list of text labels and checkbox icons next to each. +// Changing the selection (any number can be selected) will trigger the changed func. +// +// Since: 2.1 +type CheckGroup struct { + DisableableWidget + Horizontal bool + Required bool + OnChanged func([]string) `json:"-"` + Options []string + Selected []string + + items []*Check +} + +// NewCheckGroup creates a new check group widget with the set options and change handler +// +// Since: 2.1 +func NewCheckGroup(options []string, changed func([]string)) *CheckGroup { + r := &CheckGroup{ + Options: options, + OnChanged: changed, + } + r.ExtendBaseWidget(r) + r.update() + return r +} + +// Append adds a new option to the end of a CheckGroup widget. +func (r *CheckGroup) Append(option string) { + r.Options = append(r.Options, option) + + r.Refresh() +} + +// CreateRenderer is a private method to Fyne which links this widget to its renderer +func (r *CheckGroup) CreateRenderer() fyne.WidgetRenderer { + r.ExtendBaseWidget(r) + + r.update() + objects := make([]fyne.CanvasObject, len(r.items)) + for i, item := range r.items { + objects[i] = item + } + + return &checkGroupRenderer{widget.NewBaseRenderer(objects), r.items, r} +} + +// MinSize returns the size that this widget should not shrink below +func (r *CheckGroup) MinSize() fyne.Size { + r.ExtendBaseWidget(r) + return r.BaseWidget.MinSize() +} + +// Refresh causes this widget to be redrawn in it's current state. +func (r *CheckGroup) Refresh() { + r.update() + r.BaseWidget.Refresh() +} + +// Remove removes the first occurrence of the specified option found from a CheckGroup widget. +// Return true if an option was removed. +// +// Since: 2.3 +func (r *CheckGroup) Remove(option string) bool { + for i, o := range r.Options { + if strings.EqualFold(option, o) { + r.Options = append(r.Options[:i], r.Options[i+1:]...) + for j, s := range r.Selected { + if strings.EqualFold(option, s) { + r.Selected = append(r.Selected[:j], r.Selected[j+1:]...) + break + } + } + r.Refresh() + return true + } + } + return false +} + +// SetSelected sets the checked options, it can be used to set a default option. +func (r *CheckGroup) SetSelected(options []string) { + //if r.Selected == options { + // return + //} + + r.Selected = options + + if r.OnChanged != nil { + r.OnChanged(options) + } + + r.Refresh() +} + +func (r *CheckGroup) itemTapped(item *Check) { + if r.Disabled() { + return + } + + contains := false + for i, s := range r.Selected { + if s == item.Text { + contains = true + if len(r.Selected) <= 1 { + if r.Required { + item.SetChecked(true) + return + } + r.Selected = nil + } else { + r.Selected = append(r.Selected[:i], r.Selected[i+1:]...) + } + break + } + } + + if !contains { + r.Selected = append(r.Selected, item.Text) + } + + if r.OnChanged != nil { + r.OnChanged(r.Selected) + } + r.Refresh() +} + +func (r *CheckGroup) update() { + r.Options = removeDuplicates(r.Options) + if len(r.items) < len(r.Options) { + for i := len(r.items); i < len(r.Options); i++ { + var item *Check + item = NewCheck(r.Options[i], func(bool) { + r.itemTapped(item) + }) + r.items = append(r.items, item) + } + } else if len(r.items) > len(r.Options) { + r.items = r.items[:len(r.Options)] + } + for i, item := range r.items { + contains := false + for _, s := range r.Selected { + if s == item.Text { + contains = true + break + } + } + + item.Text = r.Options[i] + item.Checked = contains + item.DisableableWidget.disabled = r.Disabled() + item.Refresh() + } +} + +type checkGroupRenderer struct { + widget.BaseRenderer + items []*Check + checks *CheckGroup +} + +// Layout the components of the checks widget +func (r *checkGroupRenderer) Layout(_ fyne.Size) { + count := 1 + if len(r.items) > 0 { + count = len(r.items) + } + var itemHeight, itemWidth float32 + minSize := r.checks.MinSize() + if r.checks.Horizontal { + itemHeight = minSize.Height + itemWidth = minSize.Width / float32(count) + } else { + itemHeight = minSize.Height / float32(count) + itemWidth = minSize.Width + } + + itemSize := fyne.NewSize(itemWidth, itemHeight) + x, y := float32(0), float32(0) + for _, item := range r.items { + item.Resize(itemSize) + item.Move(fyne.NewPos(x, y)) + if r.checks.Horizontal { + x += itemWidth + } else { + y += itemHeight + } + } +} + +// MinSize calculates the minimum size of a checks item. +// This is based on the contained text, the checks icon and a standard amount of padding +// between each item. +func (r *checkGroupRenderer) MinSize() fyne.Size { + width := float32(0) + height := float32(0) + for _, item := range r.items { + itemMin := item.MinSize() + + width = fyne.Max(width, itemMin.Width) + height = fyne.Max(height, itemMin.Height) + } + + if r.checks.Horizontal { + width = width * float32(len(r.items)) + } else { + height = height * float32(len(r.items)) + } + + return fyne.NewSize(width, height) +} + +func (r *checkGroupRenderer) Refresh() { + r.updateItems() + canvas.Refresh(r.checks.super()) +} + +func (r *checkGroupRenderer) updateItems() { + if len(r.items) < len(r.checks.Options) { + for i := len(r.items); i < len(r.checks.Options); i++ { + var item *Check + item = NewCheck(r.checks.Options[i], func(bool) { + r.checks.itemTapped(item) + }) + r.SetObjects(append(r.Objects(), item)) + r.items = append(r.items, item) + } + r.Layout(r.checks.Size()) + } else if len(r.items) > len(r.checks.Options) { + total := len(r.checks.Options) + r.items = r.items[:total] + r.SetObjects(r.Objects()[:total]) + } + for i, item := range r.items { + contains := false + for _, s := range r.checks.Selected { + if s == item.Text { + contains = true + break + } + } + item.Text = r.checks.Options[i] + item.Checked = contains + item.disabled = r.checks.Disabled() + item.Refresh() + } +} + +func removeDuplicates(options []string) []string { + var result []string + found := make(map[string]bool) + + for _, option := range options { + if _, ok := found[option]; !ok { + found[option] = true + result = append(result, option) + } + } + + return result +} diff --git a/vendor/fyne.io/fyne/v2/widget/date_entry.go b/vendor/fyne.io/fyne/v2/widget/date_entry.go new file mode 100644 index 0000000..6aff0a9 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/date_entry.go @@ -0,0 +1,160 @@ +package widget + +import ( + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/theme" +) + +var ( + _ fyne.Widget = (*DateEntry)(nil) + _ fyne.Tappable = (*DateEntry)(nil) + _ fyne.Disableable = (*DateEntry)(nil) +) + +// DateEntry is an input field which supports selecting from a fixed set of options. +// +// Since: 2.6 +type DateEntry struct { + Entry + Date *time.Time + OnChanged func(*time.Time) `json:"-"` + + dropDown *Calendar + popUp *PopUp +} + +// NewDateEntry creates a date input where the date can be selected or typed. +// +// Since: 2.6 +func NewDateEntry() *DateEntry { + e := &DateEntry{} + e.ExtendBaseWidget(e) + e.Wrapping = fyne.TextWrap(fyne.TextTruncateClip) + return e +} + +// CreateRenderer returns a new renderer for this select entry. +func (e *DateEntry) CreateRenderer() fyne.WidgetRenderer { + e.ExtendBaseWidget(e) + + dateFormat := getLocaleDateFormat() + e.Validator = func(in string) error { + _, err := time.Parse(dateFormat, in) + return err + } + e.Entry.OnChanged = func(in string) { + if in == "" { + e.Date = nil + + if f := e.OnChanged; f != nil { + f(nil) + } + } + t, err := time.Parse(dateFormat, in) + if err != nil { + return + } + + e.Date = &t + + if f := e.OnChanged; f != nil { + f(&t) + } + } + + if e.ActionItem == nil { + e.ActionItem = e.setupDropDown() + if e.Disabled() { + e.ActionItem.(fyne.Disableable).Disable() + } + } + + return e.Entry.CreateRenderer() +} + +// Enable this widget, updating any style or features appropriately. +func (e *DateEntry) Enable() { + if e.ActionItem != nil { + if d, ok := e.ActionItem.(fyne.Disableable); ok { + d.Enable() + } + } + e.Entry.Enable() +} + +// Disable this widget so that it cannot be interacted with, updating any style appropriately. +func (e *DateEntry) Disable() { + if e.ActionItem != nil { + if d, ok := e.ActionItem.(fyne.Disableable); ok { + d.Disable() + } + } + e.Entry.Disable() +} + +// MinSize returns the minimal size of the select entry. +func (e *DateEntry) MinSize() fyne.Size { + e.ExtendBaseWidget(e) + return e.Entry.MinSize() +} + +// Move changes the relative position of the date entry. +func (e *DateEntry) Move(pos fyne.Position) { + e.Entry.Move(pos) + if e.popUp != nil { + e.popUp.Move(e.popUpPos()) + } +} + +// Resize changes the size of the date entry. +func (e *DateEntry) Resize(size fyne.Size) { + e.Entry.Resize(size) + if e.popUp != nil { + e.popUp.Resize(fyne.NewSize(size.Width, e.popUp.Size().Height)) + } +} + +// SetDate will update the widget to a specific date. +// You can pass nil to unselect a date. +func (e *DateEntry) SetDate(d *time.Time) { + if d == nil { + e.Date = nil + e.Entry.SetText("") + + return + } + + e.setDate(*d) +} + +func (e *DateEntry) popUpPos() fyne.Position { + entryPos := fyne.CurrentApp().Driver().AbsolutePositionForObject(e.super()) + return entryPos.Add(fyne.NewPos(0, e.Size().Height-e.Theme().Size(theme.SizeNameInputBorder))) +} + +func (e *DateEntry) setDate(d time.Time) { + e.Date = &d + if e.popUp != nil { + e.popUp.Hide() + } + + e.Entry.SetText(d.Format(getLocaleDateFormat())) +} + +func (e *DateEntry) setupDropDown() *Button { + if e.dropDown == nil { + e.dropDown = NewCalendar(time.Now(), e.setDate) + } + dropDownButton := NewButton("", func() { + c := fyne.CurrentApp().Driver().CanvasForObject(e.super()) + + e.popUp = NewPopUp(e.dropDown, c) + e.popUp.ShowAtPosition(e.popUpPos()) + e.popUp.Resize(fyne.NewSize(e.Size().Width, e.popUp.MinSize().Height)) + }) + dropDownButton.Importance = LowImportance + dropDownButton.SetIcon(e.Theme().Icon(theme.IconNameCalendar)) + return dropDownButton +} diff --git a/vendor/fyne.io/fyne/v2/widget/entry.go b/vendor/fyne.io/fyne/v2/widget/entry.go new file mode 100644 index 0000000..5f6d465 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/entry.go @@ -0,0 +1,2166 @@ +package widget + +import ( + "image/color" + "runtime" + "strings" + "time" + "unicode" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/data/binding" + "fyne.io/fyne/v2/driver/desktop" + "fyne.io/fyne/v2/driver/mobile" + "fyne.io/fyne/v2/internal/cache" + "fyne.io/fyne/v2/internal/widget" + "fyne.io/fyne/v2/lang" + "fyne.io/fyne/v2/theme" +) + +const ( + bindIgnoreDelay = time.Millisecond * 100 // ignore incoming DataItem fire after we have called Set + multiLineRows = 3 +) + +// Declare conformity with interfaces +var ( + _ fyne.Disableable = (*Entry)(nil) + _ fyne.Draggable = (*Entry)(nil) + _ fyne.Focusable = (*Entry)(nil) + _ fyne.Tappable = (*Entry)(nil) + _ fyne.Widget = (*Entry)(nil) + _ desktop.Mouseable = (*Entry)(nil) + _ desktop.Keyable = (*Entry)(nil) + _ mobile.Keyboardable = (*Entry)(nil) + _ mobile.Touchable = (*Entry)(nil) + _ fyne.Tabbable = (*Entry)(nil) +) + +// Entry widget allows simple text to be input when focused. +type Entry struct { + DisableableWidget + shortcut fyne.ShortcutHandler + Text string + // Since: 2.0 + TextStyle fyne.TextStyle + PlaceHolder string + OnChanged func(string) `json:"-"` + // Since: 2.0 + OnSubmitted func(string) `json:"-"` + Password bool + MultiLine bool + Wrapping fyne.TextWrap + + // Scroll can be used to turn off the scrolling of our entry when Wrapping is WrapNone. + // + // Since: 2.4 + Scroll fyne.ScrollDirection + + // Set a validator that this entry will check against + // Since: 1.4 + Validator fyne.StringValidator `json:"-"` + validationStatus *validationStatus + onValidationChanged func(error) + validationError error + + // If true, the Validator runs automatically on render without user interaction. + // It will reflect any validation errors found or those explicitly set via SetValidationError(). + // Since: 2.7 + AlwaysShowValidationError bool + + CursorRow, CursorColumn int + OnCursorChanged func() `json:"-"` + + // Icon is displayed at the outer left of the entry. + // It is not clickable, but can be used to indicate the purpose of the entry. + // Since: 2.7 + Icon fyne.Resource `json:"-"` + + cursorAnim *entryCursorAnimation + + dirty bool + focused bool + text RichText + placeholder RichText + content *entryContent + scroll *widget.Scroll + + // useful for Form validation (as the error text should only be shown when + // the entry is unfocused) + onFocusChanged func(bool) + + // selectKeyDown indicates whether left shift or right shift is currently held down + selectKeyDown bool + + sel *selectable + popUp *PopUpMenu + // TODO: Add OnSelectChanged + + // ActionItem is a small item which is displayed at the outer right of the entry (like a password revealer) + ActionItem fyne.CanvasObject `json:"-"` + binder basicBinder + conversionError error + minCache fyne.Size + multiLineRows int // override global default number of visible lines + + // undoStack stores the data necessary for undo/redo functionality + // See entryUndoStack for implementation details. + undoStack entryUndoStack +} + +// NewEntry creates a new single line entry widget. +func NewEntry() *Entry { + e := &Entry{Wrapping: fyne.TextWrap(fyne.TextTruncateClip)} + e.ExtendBaseWidget(e) + return e +} + +// NewEntryWithData returns an Entry widget connected to the specified data source. +// +// Since: 2.0 +func NewEntryWithData(data binding.String) *Entry { + entry := NewEntry() + entry.Bind(data) + + return entry +} + +// NewMultiLineEntry creates a new entry that allows multiple lines +func NewMultiLineEntry() *Entry { + e := &Entry{MultiLine: true, Wrapping: fyne.TextWrap(fyne.TextTruncateClip)} + e.ExtendBaseWidget(e) + return e +} + +// NewPasswordEntry creates a new entry password widget +func NewPasswordEntry() *Entry { + e := &Entry{Password: true, Wrapping: fyne.TextWrap(fyne.TextTruncateClip)} + e.ExtendBaseWidget(e) + e.ActionItem = newPasswordRevealer(e) + return e +} + +// AcceptsTab returns if Entry accepts the Tab key or not. +// +// Since: 2.1 +func (e *Entry) AcceptsTab() bool { + return e.MultiLine +} + +// Bind connects the specified data source to this Entry. +// The current value will be displayed and any changes in the data will cause the widget to update. +// User interactions with this Entry will set the value into the data source. +// +// Since: 2.0 +func (e *Entry) Bind(data binding.String) { + e.binder.SetCallback(e.updateFromData) + e.binder.Bind(data) + + e.Validator = func(string) error { + return e.conversionError + } +} + +// CreateRenderer is a private method to Fyne which links this widget to its renderer +func (e *Entry) CreateRenderer() fyne.WidgetRenderer { + th := e.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + e.ExtendBaseWidget(e) + + // initialise + e.textProvider() + e.placeholderProvider() + e.syncSelectable() + + box := canvas.NewRectangle(th.Color(theme.ColorNameInputBackground, v)) + box.CornerRadius = th.Size(theme.SizeNameInputRadius) + border := canvas.NewRectangle(color.Transparent) + border.StrokeWidth = th.Size(theme.SizeNameInputBorder) + border.StrokeColor = th.Color(theme.ColorNameInputBorder, v) + border.CornerRadius = th.Size(theme.SizeNameInputRadius) + cursor := canvas.NewRectangle(color.Transparent) + cursor.Hide() + + e.cursorAnim = newEntryCursorAnimation(cursor) + e.content = &entryContent{entry: e} + e.scroll = widget.NewScroll(nil) + objects := []fyne.CanvasObject{box, border} + if e.Wrapping != fyne.TextWrapOff || e.Scroll != widget.ScrollNone { + e.scroll.Content = e.content + objects = append(objects, e.scroll) + } else { + e.scroll.Hide() + objects = append(objects, e.content) + } + e.content.scroll = e.scroll + + if e.Password && e.ActionItem == nil { + // An entry widget has been created via struct setting manually + // the Password field to true. Going to enable the password revealer. + e.ActionItem = newPasswordRevealer(e) + } + + if e.ActionItem != nil { + objects = append(objects, e.ActionItem) + } + + icon := canvas.NewImageFromResource(e.Icon) + icon.FillMode = canvas.ImageFillContain + objects = append(objects, icon) + if e.Icon == nil { + icon.Hide() + } + + e.syncSegments() + return &entryRenderer{box, border, e.scroll, icon, objects, e} +} + +// CursorPosition returns the relative position of this Entry widget's cursor. +// +// Since: 2.7 +func (e *Entry) CursorPosition() fyne.Position { + provider := e.textProvider() + th := e.Theme() + innerPad := th.Size(theme.SizeNameInnerPadding) + inputBorder := th.Size(theme.SizeNameInputBorder) + textSize := th.Size(theme.SizeNameText) + + size := provider.lineSizeToColumn(e.CursorColumn, e.CursorRow, textSize, innerPad) + xPos := size.Width + yPos := size.Height * float32(e.CursorRow) + + return fyne.NewPos(xPos-(inputBorder/2), yPos+innerPad-inputBorder) +} + +// CursorTextOffset returns how many runes into the source text the cursor is positioned at. +// +// Since: 2.7 +func (e *Entry) CursorTextOffset() (pos int) { + return textPosFromRowCol(e.CursorRow, e.CursorColumn, e.textProvider()) +} + +// Cursor returns the cursor type of this widget +func (e *Entry) Cursor() desktop.Cursor { + return desktop.TextCursor +} + +// DoubleTapped is called when this entry has been double tapped so we should select text below the pointer +func (e *Entry) DoubleTapped(_ *fyne.PointEvent) { + e.focused = true + e.syncSelectable() + e.sel.doubleTappedAtUnixMillis = time.Now().UnixMilli() + row := e.textProvider().row(e.CursorRow) + start, end := getTextWhitespaceRegion(row, e.CursorColumn, false) + if start == -1 || end == -1 { + return + } + + e.setFieldsAndRefresh(func() { + if !e.selectKeyDown { + e.sel.selectRow = e.CursorRow + e.sel.selectColumn = start + } + // Always aim to maximise the selected region + if e.sel.selectRow > e.CursorRow || (e.sel.selectRow == e.CursorRow && e.sel.selectColumn > e.CursorColumn) { + e.CursorColumn = start + } else { + e.CursorColumn = end + } + + e.syncSelectable() + e.sel.selecting = true + }) +} + +// DragEnd is called at end of a drag event. +func (e *Entry) DragEnd() { + e.syncSelectable() + + if e.CursorColumn == e.sel.selectColumn && e.CursorRow == e.sel.selectRow { + e.sel.selecting = false + } +} + +// Dragged is called when the pointer moves while a button is held down. +// It updates the selection accordingly. +func (e *Entry) Dragged(d *fyne.DragEvent) { + d.Position = d.Position.Add(fyne.NewPos(0, e.Theme().Size(theme.SizeNameInputBorder))) + e.sel.dragged(d) + e.updateMousePointer(d.Position, false) +} + +// ExtendBaseWidget is used by an extending widget to make use of BaseWidget functionality. +func (e *Entry) ExtendBaseWidget(wid fyne.Widget) { + e.BaseWidget.ExtendBaseWidget(wid) + e.registerShortcut() +} + +// FocusGained is called when the Entry has been given focus. +func (e *Entry) FocusGained() { + e.setFieldsAndRefresh(func() { + e.dirty = true + e.focused = true + }) + if e.onFocusChanged != nil { + e.onFocusChanged(true) + } +} + +// FocusLost is called when the Entry has had focus removed. +func (e *Entry) FocusLost() { + e.setFieldsAndRefresh(func() { + e.focused = false + e.selectKeyDown = false + }) + if e.onFocusChanged != nil { + e.onFocusChanged(false) + } +} + +// Hide hides the entry. +func (e *Entry) Hide() { + if e.popUp != nil { + e.popUp.Hide() + e.popUp = nil + } + e.DisableableWidget.Hide() +} + +// Keyboard implements the Keyboardable interface +func (e *Entry) Keyboard() mobile.KeyboardType { + if e.MultiLine { + return mobile.DefaultKeyboard + } else if e.Password { + return mobile.PasswordKeyboard + } + + return mobile.SingleLineKeyboard +} + +// KeyDown handler for keypress events - used to store shift modifier state for text selection +func (e *Entry) KeyDown(key *fyne.KeyEvent) { + if e.Disabled() { + return + } + // For keyboard cursor controlled selection we now need to store shift key state and selection "start" + // Note: selection start is where the highlight started (if the user moves the selection up or left then + // the selectRow/Column will not match SelectionStart) + if key.Name == desktop.KeyShiftLeft || key.Name == desktop.KeyShiftRight { + if !e.sel.selecting { + e.sel.selectRow = e.CursorRow + e.sel.selectColumn = e.CursorColumn + } + e.selectKeyDown = true + } +} + +// KeyUp handler for key release events - used to reset shift modifier state for text selection +func (e *Entry) KeyUp(key *fyne.KeyEvent) { + if e.Disabled() { + return + } + // Handle shift release for keyboard selection + // Note: if shift is released then the user may repress it without moving to adjust their old selection + if key.Name == desktop.KeyShiftLeft || key.Name == desktop.KeyShiftRight { + e.selectKeyDown = false + } +} + +// MinSize returns the size that this widget should not shrink below. +func (e *Entry) MinSize() fyne.Size { + cached := e.minCache + if !cached.IsZero() { + return cached + } + + e.ExtendBaseWidget(e) + min := e.BaseWidget.MinSize() + + e.minCache = min + return min +} + +// MouseDown called on mouse click, this triggers a mouse click which can move the cursor, +// update the existing selection (if shift is held), or start a selection dragging operation. +func (e *Entry) MouseDown(m *desktop.MouseEvent) { + e.requestFocus() + e.syncSelectable() + + if isTripleTap(e.sel.doubleTappedAtUnixMillis, time.Now().UnixMilli()) { + e.sel.selectCurrentRow(false) + e.CursorColumn = e.sel.cursorColumn + e.Refresh() + return + } + if e.selectKeyDown { + e.sel.selecting = true + } + if e.sel.selecting && !e.selectKeyDown && m.Button == desktop.MouseButtonPrimary { + e.sel.selecting = false + } + + e.updateMousePointer(m.Position.Add(e.scroll.Offset), m.Button == desktop.MouseButtonSecondary) + + if !e.Disabled() { + e.requestFocus() + } +} + +// MouseUp called on mouse release +// If a mouse drag event has completed then check to see if it has resulted in an empty selection, +// if so, and if a text select key isn't held, then disable selecting +func (e *Entry) MouseUp(m *desktop.MouseEvent) { + e.syncSelectable() + start, _ := e.sel.selection() + if start == -1 && e.sel.selecting && !e.selectKeyDown { + e.sel.selecting = false + } +} + +// Redo un-does the last undo action. +// +// Since: 2.5 +func (e *Entry) Redo() { + newText, action := e.undoStack.Redo(e.Text) + modify, ok := action.(*entryModifyAction) + if !ok { + return + } + pos := modify.Position + if !modify.Delete { + pos += len(modify.Text) + } + e.updateText(newText, false) + e.CursorRow, e.CursorColumn = e.rowColFromTextPos(pos) + e.syncSelectable() + if e.OnChanged != nil { + e.OnChanged(newText) + } + e.Refresh() +} + +func (e *Entry) Refresh() { + e.minCache = fyne.Size{} + + if e.sel != nil { + e.sel.style = e.TextStyle + e.sel.theme = e.Theme() + e.sel.focussed = e.focused + e.sel.Refresh() + } + e.BaseWidget.Refresh() +} + +// SelectedText returns the text currently selected in this Entry. +// If there is no selection it will return the empty string. +func (e *Entry) SelectedText() string { + return e.sel.SelectedText() +} + +// SetIcon sets the leading icon resource for the entry. +// The icon will be displayed at the outer left of the entry, but is not clickable. +// This can be used to indicate the purpose of the entry, such as an email or password field. +// +// Since: 2.7 +func (e *Entry) SetIcon(res fyne.Resource) { + e.Icon = res + e.Refresh() +} + +// SetMinRowsVisible forces a multi-line entry to show `count` number of rows without scrolling. +// This is not a validation or requirement, it just impacts the minimum visible size. +// Use this carefully as Fyne apps can run on small screens so you may wish to add a scroll container if +// this number is high. Default is 3. +// +// Since: 2.2 +func (e *Entry) SetMinRowsVisible(count int) { + e.multiLineRows = count + e.Refresh() +} + +// SetPlaceHolder sets the text that will be displayed if the entry is otherwise empty +func (e *Entry) SetPlaceHolder(text string) { + e.Theme() // setup theme cache before locking + + e.PlaceHolder = text + + e.placeholderProvider().Segments[0].(*TextSegment).Text = text + e.placeholder.updateRowBounds() + e.placeholderProvider().Refresh() +} + +// SetText manually sets the text of the Entry to the given text value. +// Calling SetText resets all undo history. +func (e *Entry) SetText(text string) { + e.setText(text, false) +} + +func (e *Entry) setText(text string, fromBinding bool) { + e.Theme() // setup theme cache before locking + e.updateTextAndRefresh(text, fromBinding) + e.updateCursorAndSelection() + + e.undoStack.Clear() +} + +// Append appends the text to the end of the entry. +// +// Since: 2.4 +func (e *Entry) Append(text string) { + provider := e.textProvider() + provider.insertAt(provider.len(), []rune(text)) + content := provider.String() + changed := e.updateText(content, false) + cb := e.OnChanged + e.undoStack.Clear() + + if changed { + e.validate() + if cb != nil { + cb(content) + } + } + e.Refresh() +} + +// Tapped is called when this entry has been tapped. We update the cursor position in +// device-specific callbacks (MouseDown() and TouchDown()). +func (e *Entry) Tapped(ev *fyne.PointEvent) { + if fyne.CurrentDevice().IsMobile() && e.sel.selecting { + e.sel.selecting = false + } +} + +// TappedSecondary is called when right or alternative tap is invoked. +// +// Opens the PopUpMenu with `Paste` item to paste text from the clipboard. +func (e *Entry) TappedSecondary(pe *fyne.PointEvent) { + if e.Disabled() && e.Password { + return // no popup options for a disabled concealed field + } + + e.requestFocus() + + super := e.super() + app := fyne.CurrentApp() + clipboard := app.Clipboard() + typedShortcut := super.(fyne.Shortcutable).TypedShortcut + cutItem := fyne.NewMenuItem(lang.L("Cut"), func() { + typedShortcut(&fyne.ShortcutCut{Clipboard: clipboard}) + }) + copyItem := fyne.NewMenuItem(lang.L("Copy"), func() { + typedShortcut(&fyne.ShortcutCopy{Clipboard: clipboard}) + }) + pasteItem := fyne.NewMenuItem(lang.L("Paste"), func() { + typedShortcut(&fyne.ShortcutPaste{Clipboard: clipboard}) + }) + selectAllItem := fyne.NewMenuItem(lang.L("Select all"), e.selectAll) + + menuItems := make([]*fyne.MenuItem, 0, 6) + if e.Disabled() { + menuItems = append(menuItems, copyItem, selectAllItem) + } else if e.Password { + menuItems = append(menuItems, pasteItem, selectAllItem) + } else { + canUndo, canRedo := e.undoStack.CanUndo(), e.undoStack.CanRedo() + if canUndo { + undoItem := fyne.NewMenuItem(lang.L("Undo"), e.Undo) + menuItems = append(menuItems, undoItem) + } + if canRedo { + redoItem := fyne.NewMenuItem(lang.L("Redo"), e.Redo) + menuItems = append(menuItems, redoItem) + } + if canUndo || canRedo { + menuItems = append(menuItems, fyne.NewMenuItemSeparator()) + } + menuItems = append(menuItems, cutItem, copyItem, pasteItem, selectAllItem) + } + + driver := app.Driver() + entryPos := driver.AbsolutePositionForObject(super) + popUpPos := entryPos.Add(pe.Position) + e.popUp = NewPopUpMenu(fyne.NewMenu("", menuItems...), driver.CanvasForObject(super)) + e.popUp.ShowAtPosition(popUpPos) +} + +// TouchDown is called when this entry gets a touch down event on mobile device, we ensure we have focus. +// +// Since: 2.1 +func (e *Entry) TouchDown(ev *mobile.TouchEvent) { + now := time.Now().UnixMilli() + e.syncSegments() + if !e.Disabled() { + e.requestFocus() + } + if isTripleTap(e.sel.doubleTappedAtUnixMillis, now) { + e.sel.selectCurrentRow(false) + e.CursorColumn = e.sel.cursorColumn + e.Refresh() + return + } + + e.updateMousePointer(ev.Position, false) +} + +// TouchUp is called when this entry gets a touch up event on mobile device. +// +// Since: 2.1 +func (e *Entry) TouchUp(*mobile.TouchEvent) { +} + +// TouchCancel is called when this entry gets a touch cancel event on mobile device (app was removed from focus). +// +// Since: 2.1 +func (e *Entry) TouchCancel(*mobile.TouchEvent) { +} + +// TypedKey receives key input events when the Entry widget is focused. +func (e *Entry) TypedKey(key *fyne.KeyEvent) { + if e.Disabled() { + return + } + if e.cursorAnim != nil { + e.cursorAnim.interrupt() + } + provider := e.textProvider() + multiLine := e.MultiLine + + if e.selectKeyDown || e.sel.selecting { + if e.selectingKeyHandler(key) { + e.Refresh() + return + } + } + + switch key.Name { + case fyne.KeyBackspace: + isEmpty := provider.len() == 0 || (e.CursorColumn == 0 && e.CursorRow == 0) + if isEmpty { + return + } + + pos := e.CursorTextOffset() + deletedText := provider.deleteFromTo(pos-1, pos) + e.CursorRow, e.CursorColumn = e.rowColFromTextPos(pos - 1) + e.syncSelectable() + e.undoStack.MergeOrAdd(&entryModifyAction{ + Delete: true, + Position: pos - 1, + Text: deletedText, + }) + case fyne.KeyDelete: + pos := e.CursorTextOffset() + if provider.len() == 0 || pos == provider.len() { + return + } + + deletedText := provider.deleteFromTo(pos, pos+1) + e.undoStack.MergeOrAdd(&entryModifyAction{ + Delete: true, + Position: pos, + Text: deletedText, + }) + case fyne.KeyReturn, fyne.KeyEnter: + e.typedKeyReturn(provider, multiLine) + case fyne.KeyTab: + e.typedKeyTab() + case fyne.KeyUp: + e.typedKeyUp(provider) + case fyne.KeyDown: + e.typedKeyDown(provider) + case fyne.KeyLeft: + e.typedKeyLeft(provider) + case fyne.KeyRight: + e.typedKeyRight(provider) + case fyne.KeyEnd: + e.typedKeyEnd(provider) + case fyne.KeyHome: + e.typedKeyHome() + case fyne.KeyPageUp: + if e.MultiLine { + e.CursorRow = 0 + } + e.CursorColumn = 0 + e.syncSelectable() + case fyne.KeyPageDown: + if e.MultiLine { + e.CursorRow = provider.rows() - 1 + e.CursorColumn = provider.rowLength(e.CursorRow) + } else { + e.CursorColumn = provider.len() + } + e.syncSelectable() + default: + return + } + + content := provider.String() + changed := e.updateText(content, false) + if e.CursorRow == e.sel.selectRow && e.CursorColumn == e.sel.selectColumn { + e.sel.selecting = false + } + cb := e.OnChanged + if changed { + e.validate() + if cb != nil { + cb(content) + } + } + e.Refresh() +} + +// Undo un-does the last modifying user-action. +// +// Since: 2.5 +func (e *Entry) Undo() { + newText, action := e.undoStack.Undo(e.Text) + modify, ok := action.(*entryModifyAction) + if !ok { + return + } + pos := modify.Position + if modify.Delete { + pos += len(modify.Text) + } + e.updateText(newText, false) + e.CursorRow, e.CursorColumn = e.rowColFromTextPos(pos) + e.syncSelectable() + if e.OnChanged != nil { + e.OnChanged(newText) + } + e.Refresh() +} + +func (e *Entry) typedKeyUp(provider *RichText) { + if e.CursorRow > 0 { + e.CursorRow-- + } else { + e.CursorColumn = 0 + } + + rowLength := provider.rowLength(e.CursorRow) + if e.CursorColumn > rowLength { + e.CursorColumn = rowLength + } + e.syncSelectable() +} + +func (e *Entry) typedKeyDown(provider *RichText) { + rowLength := provider.rowLength(e.CursorRow) + + if e.CursorRow < provider.rows()-1 { + e.CursorRow++ + rowLength = provider.rowLength(e.CursorRow) + } else { + e.CursorColumn = rowLength + } + + if e.CursorColumn > rowLength { + e.CursorColumn = rowLength + } + e.syncSelectable() +} + +func (e *Entry) typedKeyLeft(provider *RichText) { + if e.CursorColumn > 0 { + e.CursorColumn-- + } else if e.MultiLine && e.CursorRow > 0 { + e.CursorRow-- + e.CursorColumn = provider.rowLength(e.CursorRow) + } + e.syncSelectable() +} + +func (e *Entry) typedKeyRight(provider *RichText) { + if e.MultiLine { + rowLength := provider.rowLength(e.CursorRow) + if e.CursorColumn < rowLength { + e.CursorColumn++ + } else if e.CursorRow < provider.rows()-1 { + e.CursorRow++ + e.CursorColumn = 0 + } + } else if e.CursorColumn < provider.len() { + e.CursorColumn++ + } + e.syncSelectable() +} + +func (e *Entry) typedKeyHome() { + e.CursorColumn = 0 +} + +func (e *Entry) typedKeyEnd(provider *RichText) { + if e.MultiLine { + e.CursorColumn = provider.rowLength(e.CursorRow) + } else { + e.CursorColumn = provider.len() + } +} + +// handler for Ctrl+[backspace/delete] - delete the word +// to the left or right of the cursor +func (e *Entry) deleteWord(right bool) { + provider := e.textProvider() + cursorRow, cursorCol := e.CursorRow, e.CursorColumn + + // start, end relative to text row + start, end := getTextWhitespaceRegion(provider.row(cursorRow), cursorCol, true) + if right { + start = cursorCol + } else { + end = cursorCol + } + if start == -1 || end == -1 { + return + } + + // convert start, end to absolute text position + b := provider.rowBoundary(cursorRow) + if b != nil { + start += b.begin + end += b.begin + } + + erased := provider.deleteFromTo(start, end) + e.undoStack.MergeOrAdd(&entryModifyAction{ + Delete: true, + Position: start, + Text: erased, + }) + + if !right { + e.CursorColumn = cursorCol - (end - start) + } + e.updateTextAndRefresh(provider.String(), false) +} + +func (e *Entry) typedKeyTab() { + if dd, ok := fyne.CurrentApp().Driver().(desktop.Driver); ok { + if dd.CurrentKeyModifiers()&fyne.KeyModifierShift != 0 { + return // don't insert a tab when Shift+Tab typed + } + } + e.TypedRune('\t') +} + +// TypedRune receives text input events when the Entry widget is focused. +func (e *Entry) TypedRune(r rune) { + if e.Disabled() { + return + } + + e.syncSelectable() + if e.popUp != nil { + e.popUp.Hide() + } + + // if we've typed a character and we're selecting then replace the selection with the character + cb := e.OnChanged + if e.sel.selecting { + e.eraseSelection() + } + + runes := []rune{r} + pos := e.CursorTextOffset() + + provider := e.textProvider() + provider.insertAt(pos, runes) + + content := provider.String() + e.updateText(content, false) + e.CursorRow, e.CursorColumn = e.rowColFromTextPos(pos + len(runes)) + e.syncSelectable() + + e.undoStack.MergeOrAdd(&entryModifyAction{ + Position: pos, + Text: runes, + }) + + e.validate() + if cb != nil { + cb(content) + } + e.Refresh() +} + +// TypedShortcut implements the Shortcutable interface +func (e *Entry) TypedShortcut(shortcut fyne.Shortcut) { + e.shortcut.TypedShortcut(shortcut) +} + +// Unbind disconnects any configured data source from this Entry. +// The current value will remain at the last value of the data source. +// +// Since: 2.0 +func (e *Entry) Unbind() { + e.Validator = nil + e.binder.Unbind() +} + +// copyToClipboard copies the current selection to a given clipboard. +// This does nothing if it is a concealed entry. +func (e *Entry) copyToClipboard(clipboard fyne.Clipboard) { + if !e.sel.selecting || e.Password { + return + } + + clipboard.SetContent(e.sel.SelectedText()) +} + +// cutToClipboard copies the current selection to a given clipboard and then removes the selected text. +// This does nothing if it is a concealed entry. +func (e *Entry) cutToClipboard(clipboard fyne.Clipboard) { + if !e.sel.selecting || e.Password { + return + } + + e.copyToClipboard(clipboard) + e.eraseSelectionAndUpdate() + content := e.Text + cb := e.OnChanged + + e.validate() + if cb != nil { + cb(content) + } + e.Refresh() +} + +// eraseSelection deletes the selected text and moves the cursor but does not update the text field. +func (e *Entry) eraseSelection() bool { + if e.Disabled() { + return false + } + + provider := e.textProvider() + posA, posB := e.sel.selection() + + if posA == posB { + return false + } + + erasedText := provider.deleteFromTo(posA, posB) + e.CursorRow, e.CursorColumn = e.rowColFromTextPos(posA) + e.syncSelectable() + e.sel.selectRow, e.sel.selectColumn = e.CursorRow, e.CursorColumn + e.sel.selecting = false + + e.undoStack.MergeOrAdd(&entryModifyAction{ + Delete: true, + Position: posA, + Text: erasedText, + }) + + return true +} + +// eraseSelectionAndUpdate removes the current selected region and moves the cursor. +// It also updates the text if something has been erased. +func (e *Entry) eraseSelectionAndUpdate() { + if e.eraseSelection() { + e.updateText(e.textProvider().String(), false) + } +} + +// pasteFromClipboard inserts text from the clipboard content, +// starting from the cursor position. +func (e *Entry) pasteFromClipboard(clipboard fyne.Clipboard) { + e.syncSelectable() + text := clipboard.Content() + if text == "" { + changed := e.sel.selecting && e.eraseSelection() + + if changed { + e.Refresh() + } + + return // Nothing to paste into the text content. + } + + if !e.MultiLine { + // format clipboard content to be compatible with single line entry + text = strings.Replace(text, "\n", " ", -1) + } + + if e.sel.selecting { + e.eraseSelection() + } + + runes := []rune(text) + pos := e.CursorTextOffset() + provider := e.textProvider() + provider.insertAt(pos, runes) + + e.undoStack.Add(&entryModifyAction{ + Position: pos, + Text: runes, + }) + content := provider.String() + e.updateText(content, false) + e.CursorRow, e.CursorColumn = e.rowColFromTextPos(pos + len(runes)) + e.syncSelectable() + cb := e.OnChanged + + e.validate() + if cb != nil { + cb(content) // We know that the text has changed. + } + + e.Refresh() // placing the cursor (and refreshing) happens last +} + +// placeholderProvider returns the placeholder text handler for this entry +func (e *Entry) placeholderProvider() *RichText { + if len(e.placeholder.Segments) > 0 { + return &e.placeholder + } + + e.placeholder.Scroll = widget.ScrollNone + e.placeholder.inset = fyne.NewSize(0, e.Theme().Size(theme.SizeNameInputBorder)) + + style := RichTextStyleInline + style.ColorName = theme.ColorNamePlaceHolder + style.TextStyle = e.TextStyle + + e.placeholder.Segments = []RichTextSegment{ + &TextSegment{ + Style: style, + Text: e.PlaceHolder, + }, + } + + return &e.placeholder +} + +func (e *Entry) registerShortcut() { + e.shortcut.AddShortcut(&fyne.ShortcutUndo{}, func(se fyne.Shortcut) { + e.Undo() + }) + e.shortcut.AddShortcut(&fyne.ShortcutRedo{}, func(se fyne.Shortcut) { + e.Redo() + }) + e.shortcut.AddShortcut(&fyne.ShortcutCut{}, func(se fyne.Shortcut) { + cut := se.(*fyne.ShortcutCut) + e.cutToClipboard(cut.Clipboard) + }) + e.shortcut.AddShortcut(&fyne.ShortcutCopy{}, func(se fyne.Shortcut) { + cpy := se.(*fyne.ShortcutCopy) + e.copyToClipboard(cpy.Clipboard) + }) + e.shortcut.AddShortcut(&fyne.ShortcutPaste{}, func(se fyne.Shortcut) { + paste := se.(*fyne.ShortcutPaste) + e.pasteFromClipboard(paste.Clipboard) + }) + e.shortcut.AddShortcut(&fyne.ShortcutSelectAll{}, func(se fyne.Shortcut) { + e.selectAll() + }) + + moveWord := func(s fyne.Shortcut) { + row := e.textProvider().row(e.CursorRow) + start, end := getTextWhitespaceRegion(row, e.CursorColumn, true) + if start == -1 || end == -1 { + return + } + + e.setFieldsAndRefresh(func() { + if s.(*desktop.CustomShortcut).KeyName == fyne.KeyLeft { + if e.CursorColumn == 0 { + if e.CursorRow > 0 { + e.CursorRow-- + e.CursorColumn = len(e.textProvider().row(e.CursorRow)) + } + } else { + e.CursorColumn = start + } + } else { + if e.CursorColumn == len(e.textProvider().row(e.CursorRow)) { + if e.CursorRow < e.textProvider().rows()-1 { + e.CursorRow++ + e.CursorColumn = 0 + } + } else { + e.CursorColumn = end + } + } + e.syncSelectable() + }) + } + selectMoveWord := func(se fyne.Shortcut) { + if !e.sel.selecting { + e.sel.selectColumn = e.CursorColumn + e.sel.selectRow = e.CursorRow + e.sel.selecting = true + } + moveWord(se) + } + unselectMoveWord := func(se fyne.Shortcut) { + e.sel.selecting = false + moveWord(se) + } + + moveWordModifier := fyne.KeyModifierShortcutDefault + if runtime.GOOS == "darwin" { + moveWordModifier = fyne.KeyModifierAlt + + // Cmd+left, Cmd+right shortcuts behave like Home and End keys on Mac OS + shortcutHomeEnd := func(s fyne.Shortcut) { + e.sel.selecting = false + if s.(*desktop.CustomShortcut).KeyName == fyne.KeyLeft { + e.typedKeyHome() + } else { + e.typedKeyEnd(e.textProvider()) + } + e.Refresh() + } + e.shortcut.AddShortcut(&desktop.CustomShortcut{KeyName: fyne.KeyLeft, Modifier: fyne.KeyModifierSuper}, shortcutHomeEnd) + e.shortcut.AddShortcut(&desktop.CustomShortcut{KeyName: fyne.KeyRight, Modifier: fyne.KeyModifierSuper}, shortcutHomeEnd) + } + + e.shortcut.AddShortcut(&desktop.CustomShortcut{KeyName: fyne.KeyLeft, Modifier: moveWordModifier}, unselectMoveWord) + e.shortcut.AddShortcut(&desktop.CustomShortcut{KeyName: fyne.KeyLeft, Modifier: moveWordModifier | fyne.KeyModifierShift}, selectMoveWord) + e.shortcut.AddShortcut(&desktop.CustomShortcut{KeyName: fyne.KeyRight, Modifier: moveWordModifier}, unselectMoveWord) + e.shortcut.AddShortcut(&desktop.CustomShortcut{KeyName: fyne.KeyRight, Modifier: moveWordModifier | fyne.KeyModifierShift}, selectMoveWord) + + e.shortcut.AddShortcut(&desktop.CustomShortcut{KeyName: fyne.KeyBackspace, Modifier: moveWordModifier}, + func(fyne.Shortcut) { e.deleteWord(false) }) + e.shortcut.AddShortcut(&desktop.CustomShortcut{KeyName: fyne.KeyDelete, Modifier: moveWordModifier}, + func(fyne.Shortcut) { e.deleteWord(true) }) +} + +func (e *Entry) requestFocus() { + impl := e.super() + if c := fyne.CurrentApp().Driver().CanvasForObject(impl); c != nil { + c.Focus(impl.(fyne.Focusable)) + } +} + +// Obtains row,col from a given textual position +// expects a read or write lock to be held by the caller +func (e *Entry) rowColFromTextPos(pos int) (row int, col int) { + provider := e.textProvider() + canWrap := e.Wrapping == fyne.TextWrapBreak || e.Wrapping == fyne.TextWrapWord + totalRows := provider.rows() + for i := 0; i < totalRows; i++ { + b := provider.rowBoundary(i) + if b == nil { + continue + } + if b.begin <= pos { + if b.end < pos { + row++ + } + col = pos - b.begin + // if this gap is at `pos` and is a line wrap, increment (safe to access boundary i-1) + if canWrap && b.begin == pos && pos != 0 && provider.rowBoundary(i-1).end == b.begin && row < (totalRows-1) { + row++ + } + } else { + break + } + } + return row, col +} + +// selectAll selects all text in entry +func (e *Entry) selectAll() { + if e.textProvider().len() == 0 { + return + } + e.setFieldsAndRefresh(func() { + e.sel.selectRow = 0 + e.sel.selectColumn = 0 + + lastRow := e.textProvider().rows() - 1 + e.CursorColumn = e.textProvider().rowLength(lastRow) + e.CursorRow = lastRow + e.syncSelectable() + e.sel.selecting = true + }) +} + +// selectingKeyHandler performs keypress action in the scenario that a selection +// is either a) in progress or b) about to start +// returns true if the keypress has been fully handled +func (e *Entry) selectingKeyHandler(key *fyne.KeyEvent) bool { + if e.selectKeyDown && !e.sel.selecting { + switch key.Name { + case fyne.KeyUp, fyne.KeyDown, + fyne.KeyLeft, fyne.KeyRight, + fyne.KeyEnd, fyne.KeyHome, + fyne.KeyPageUp, fyne.KeyPageDown: + e.sel.selecting = true + } + } + + if !e.sel.selecting { + return false + } + + switch key.Name { + case fyne.KeyBackspace, fyne.KeyDelete: + // clears the selection -- return handled + e.eraseSelectionAndUpdate() + content := e.Text + cb := e.OnChanged + + e.validate() + if cb != nil { + cb(content) + } + e.Refresh() + return true + case fyne.KeyReturn, fyne.KeyEnter: + if e.MultiLine { + // clear the selection -- return unhandled to add the newline + e.setFieldsAndRefresh(e.eraseSelectionAndUpdate) + } + return false + } + + if !e.selectKeyDown { + switch key.Name { + case fyne.KeyLeft: + // seek to the start of the selection -- return handled + selectStart, _ := e.sel.selection() + e.CursorRow, e.CursorColumn = e.rowColFromTextPos(selectStart) + e.syncSelectable() + e.sel.selecting = false + return true + case fyne.KeyRight: + // seek to the end of the selection -- return handled + _, selectEnd := e.sel.selection() + e.CursorRow, e.CursorColumn = e.rowColFromTextPos(selectEnd) + e.syncSelectable() + e.sel.selecting = false + return true + case fyne.KeyUp, fyne.KeyDown, fyne.KeyEnd, fyne.KeyHome, fyne.KeyPageUp, fyne.KeyPageDown: + // cursor movement without left or right shift -- clear selection and return unhandled + e.sel.selecting = false + return false + } + } + + return false +} + +func (e *Entry) syncSegments() { + colName := theme.ColorNameForeground + wrap := e.textWrap() + disabled := e.Disabled() + if disabled { + colName = theme.ColorNameDisabled + } + + text := e.textProvider() + text.Wrapping = wrap + + textSegment := text.Segments[0].(*TextSegment) + textSegment.Text = e.Text + textSegment.Style.ColorName = colName + textSegment.Style.concealed = e.Password + textSegment.Style.TextStyle = e.TextStyle + + colName = theme.ColorNamePlaceHolder + if disabled { + colName = theme.ColorNameDisabled + } + + placeholder := e.placeholderProvider() + placeholder.Wrapping = wrap + + textSegment = placeholder.Segments[0].(*TextSegment) + textSegment.Style.ColorName = colName + textSegment.Style.TextStyle = e.TextStyle + textSegment.Text = e.PlaceHolder +} + +func (e *Entry) syncSelectable() { + if e.sel == nil { + e.sel = &selectable{theme: e.Theme(), provider: e.textProvider(), focus: e, password: e.Password, style: e.TextStyle} + e.sel.ExtendBaseWidget(e.sel) + } + + e.sel.cursorRow, e.sel.cursorColumn = e.CursorRow, e.CursorColumn +} + +// textProvider returns the text handler for this entry +func (e *Entry) textProvider() *RichText { + if len(e.text.Segments) > 0 { + return &e.text + } + + if e.Text != "" { + e.dirty = true + } + + e.text.Scroll = widget.ScrollNone + e.text.inset = fyne.NewSize(0, e.Theme().Size(theme.SizeNameInputBorder)) + e.text.Segments = []RichTextSegment{&TextSegment{Style: RichTextStyleInline, Text: e.Text}} + return &e.text +} + +// textWrap calculates the wrapping that we should apply. +func (e *Entry) textWrap() fyne.TextWrap { + if e.Wrapping == fyne.TextWrap(fyne.TextTruncateClip) { // this is now the default - but we scroll around this large content + return fyne.TextWrapOff + } + + if !e.MultiLine && (e.Wrapping == fyne.TextWrapBreak || e.Wrapping == fyne.TextWrapWord) { + fyne.LogError("Entry cannot wrap single line", nil) + e.Wrapping = fyne.TextWrap(fyne.TextTruncateClip) + return fyne.TextWrapOff + } + return e.Wrapping +} + +func (e *Entry) updateCursorAndSelection() { + e.CursorRow, e.CursorColumn = e.truncatePosition(e.CursorRow, e.CursorColumn) + + e.syncSelectable() + e.sel.selectRow, e.sel.selectColumn = e.truncatePosition(e.sel.selectRow, e.sel.selectColumn) +} + +func (e *Entry) updateFromData(data binding.DataItem) { + if data == nil { + return + } + textSource, ok := data.(binding.String) + if !ok { + return + } + + val, err := textSource.Get() + e.conversionError = err + e.validate() + if err != nil { + return + } + e.setText(val, true) +} + +func (e *Entry) truncatePosition(row, col int) (int, int) { + if e.Text == "" { + return 0, 0 + } + newRow := row + newCol := col + if row >= e.textProvider().rows() { + newRow = e.textProvider().rows() - 1 + } + rowLength := e.textProvider().rowLength(newRow) + if (newCol >= rowLength) || (newRow < row) { + newCol = rowLength + } + return newRow, newCol +} + +func (e *Entry) updateMousePointer(p fyne.Position, rightClick bool) { + row, col := e.sel.getRowCol(p) + + if !rightClick || !e.sel.selecting { + e.CursorRow = row + e.CursorColumn = col + + e.syncSelectable() + } + + if !e.sel.selecting { + e.sel.selectRow = row + e.sel.selectColumn = col + } + + r := cache.Renderer(e.content) + if r != nil { + r.(*entryContentRenderer).moveCursor() + } +} + +// updateText updates the internal text to the given value. +// It assumes that a lock exists on the widget. +func (e *Entry) updateText(text string, fromBinding bool) bool { + changed := e.Text != text + e.Text = text + e.syncSegments() + e.text.updateRowBounds() + + if e.Text != "" { + e.dirty = true + } + + if changed && !fromBinding { + if e.binder.dataListenerPair.listener != nil { + e.binder.SetCallback(nil) + e.binder.CallWithData(e.writeData) + e.binder.SetCallback(e.updateFromData) + } + } + return changed +} + +// updateTextAndRefresh updates the internal text to the given value then refreshes it. +// This should not be called under a property lock +func (e *Entry) updateTextAndRefresh(text string, fromBinding bool) { + var callback func(string) + + changed := e.updateText(text, fromBinding) + + if changed { + callback = e.OnChanged + } + + e.validate() + if callback != nil { + callback(text) + } + e.Refresh() +} + +func (e *Entry) writeData(data binding.DataItem) { + if data == nil { + return + } + textTarget, ok := data.(binding.String) + if !ok { + return + } + curValue, err := textTarget.Get() + if err == nil && curValue == e.Text { + e.conversionError = nil + return + } + e.conversionError = textTarget.Set(e.Text) +} + +func (e *Entry) typedKeyReturn(provider *RichText, multiLine bool) { + onSubmitted := e.OnSubmitted + selectDown := e.selectKeyDown + text := e.Text + + if !multiLine { + // Single line doesn't support newline. + // Call submitted callback, if any. + if onSubmitted != nil { + onSubmitted(text) + } + return + } else if selectDown && onSubmitted != nil { + // Multiline supports newline, unless shift is held and OnSubmitted is set. + onSubmitted(text) + return + } + s := []rune("\n") + pos := e.CursorTextOffset() + provider.insertAt(pos, s) + e.undoStack.MergeOrAdd(&entryModifyAction{ + Position: pos, + Text: s, + }) + e.CursorColumn = 0 + e.CursorRow++ + e.syncSelectable() +} + +func (e *Entry) setFieldsAndRefresh(f func()) { + f() + + impl := e.super() + if impl == nil { + return + } + impl.Refresh() +} + +var _ fyne.WidgetRenderer = (*entryRenderer)(nil) + +type entryRenderer struct { + box, border *canvas.Rectangle + scroll *widget.Scroll + icon *canvas.Image + + objects []fyne.CanvasObject + entry *Entry +} + +func (r *entryRenderer) Destroy() { +} + +func (r *entryRenderer) trailingInset() float32 { + th := r.entry.Theme() + xInset := float32(0) + + if r.entry.ActionItem != nil { + xInset = r.entry.ActionItem.MinSize().Width + } + + if r.entry.Validator != nil { + iconSpace := th.Size(theme.SizeNameInlineIcon) + th.Size(theme.SizeNameLineSpacing) + if r.entry.ActionItem == nil { + xInset = iconSpace + th.Size(theme.SizeNameInnerPadding) + } else { + xInset += iconSpace + } + } + + return xInset +} + +func (r *entryRenderer) leadingInset() float32 { + th := r.entry.Theme() + xInset := float32(0) + + if r.entry.Icon != nil { + xInset = th.Size(theme.SizeNameInlineIcon) + th.Size(theme.SizeNameLineSpacing) + } + + return xInset +} + +func (r *entryRenderer) Layout(size fyne.Size) { + th := r.entry.Theme() + borderSize := th.Size(theme.SizeNameInputBorder) + iconSize := th.Size(theme.SizeNameInlineIcon) + innerPad := th.Size(theme.SizeNameInnerPadding) + inputBorder := th.Size(theme.SizeNameInputBorder) + + // 0.5 is removed so on low DPI it rounds down on the trailing edge + r.border.Resize(fyne.NewSize(size.Width-borderSize-.5, size.Height-borderSize-.5)) + r.border.StrokeWidth = borderSize + r.border.Move(fyne.NewSquareOffsetPos(borderSize / 2)) + r.box.Resize(size.Subtract(fyne.NewSquareSize(borderSize * 2))) + r.box.Move(fyne.NewSquareOffsetPos(borderSize)) + + pad := theme.InputBorderSize() + actionIconSize := fyne.NewSize(0, size.Height-pad*2) + if r.entry.ActionItem != nil { + actionIconSize.Width = r.entry.ActionItem.MinSize().Width + r.entry.ActionItem.Resize(actionIconSize) + r.entry.ActionItem.Move(fyne.NewPos(size.Width-actionIconSize.Width-pad, pad)) + } + + validatorIconSize := fyne.NewSize(0, 0) + if r.entry.Validator != nil || r.entry.AlwaysShowValidationError { + validatorIconSize = fyne.NewSquareSize(iconSize) + + r.ensureValidationSetup() + r.entry.validationStatus.Resize(validatorIconSize) + + if r.entry.ActionItem == nil { + r.entry.validationStatus.Move(fyne.NewPos(size.Width-validatorIconSize.Width-innerPad, innerPad)) + } else { + r.entry.validationStatus.Move(fyne.NewPos(size.Width-validatorIconSize.Width-actionIconSize.Width, innerPad)) + } + } + + if r.entry.Icon != nil { + r.icon.Resize(fyne.NewSquareSize(iconSize)) + r.icon.Move(fyne.NewPos(innerPad, innerPad)) + } + + r.entry.textProvider().inset = fyne.NewSize(0, inputBorder) + r.entry.placeholderProvider().inset = fyne.NewSize(0, inputBorder) + entrySize := size.Subtract(fyne.NewSize(r.trailingInset()+r.leadingInset(), inputBorder*2)) + entryPos := fyne.NewPos(r.leadingInset(), inputBorder) + + prov := r.entry.textProvider() + textPos := textPosFromRowCol(r.entry.CursorRow, r.entry.CursorColumn, prov) + selectPos := textPosFromRowCol(r.entry.sel.selectRow, r.entry.sel.selectColumn, prov) + if r.entry.Wrapping == fyne.TextWrapOff && r.entry.Scroll == widget.ScrollNone { + r.entry.content.Resize(entrySize) + r.entry.content.Move(entryPos) + } else { + r.scroll.Resize(entrySize) + r.scroll.Move(entryPos) + } + + resizedTextPos := textPosFromRowCol(r.entry.CursorRow, r.entry.CursorColumn, prov) + if textPos != resizedTextPos { + r.entry.setFieldsAndRefresh(func() { + r.entry.CursorRow, r.entry.CursorColumn = r.entry.rowColFromTextPos(textPos) + r.entry.sel.cursorRow, r.entry.sel.cursorRow = r.entry.CursorRow, r.entry.CursorColumn + + if r.entry.sel.selecting { + r.entry.sel.selectRow, r.entry.sel.selectColumn = r.entry.rowColFromTextPos(selectPos) + } + }) + } +} + +// MinSize calculates the minimum size of an entry widget. +// This is based on the contained text with a standard amount of padding added. +// If MultiLine is true then we will reserve space for at leasts 3 lines +func (r *entryRenderer) MinSize() fyne.Size { + if rend := cache.Renderer(r.entry.content); rend != nil { + rend.(*entryContentRenderer).updateScrollDirections() + } + + th := r.entry.Theme() + minSize := fyne.Size{} + + if r.scroll.Direction == widget.ScrollNone { + minSize = r.entry.content.MinSize().AddWidthHeight(0, th.Size(theme.SizeNameInputBorder)*2) + } else { + innerPadding := th.Size(theme.SizeNameInnerPadding) + textSize := th.Size(theme.SizeNameText) + charMin := r.entry.placeholderProvider().charMinSize(r.entry.Password, r.entry.TextStyle, textSize) + minSize = charMin.Add(fyne.NewSquareSize(innerPadding)) + + if r.entry.MultiLine { + count := r.entry.multiLineRows + if count <= 0 { + count = multiLineRows + } + + minSize.Height = charMin.Height*float32(count) + innerPadding + } + + minSize = minSize.AddWidthHeight(innerPadding*2, innerPadding) + } + + iconSpace := th.Size(theme.SizeNameInlineIcon) + th.Size(theme.SizeNameLineSpacing) + if r.entry.ActionItem != nil { + minSize.Width += iconSpace + } + if r.entry.Validator != nil { + minSize.Width += iconSpace + } + if r.entry.Icon != nil { + minSize.Width += iconSpace + } + + return minSize +} + +func (r *entryRenderer) Objects() []fyne.CanvasObject { + return r.objects +} + +func (r *entryRenderer) Refresh() { + content := r.entry.content + focusedAppearance := r.entry.focused && !r.entry.Disabled() + scroll := r.entry.Scroll + wrapping := r.entry.Wrapping + + r.entry.syncSegments() + r.entry.text.updateRowBounds() + r.entry.placeholder.updateRowBounds() + r.entry.text.Refresh() + r.entry.placeholder.Refresh() + + th := r.entry.Theme() + inputBorder := th.Size(theme.SizeNameInputBorder) + + // correct our scroll wrappers if the wrap mode changed + entrySize := r.entry.Size().Subtract(fyne.NewSize(r.trailingInset()+r.leadingInset(), inputBorder*2)) + if wrapping == fyne.TextWrapOff && scroll == widget.ScrollNone && r.scroll.Content != nil { + r.scroll.Hide() + r.scroll.Content = nil + content.Move(fyne.NewPos(0, inputBorder)) + content.Resize(entrySize) + + for i, o := range r.objects { + if o == r.scroll { + r.objects[i] = content + break + } + } + } else if (wrapping != fyne.TextWrapOff || scroll != widget.ScrollNone) && r.scroll.Content == nil { + r.scroll.Content = content + content.Move(fyne.NewPos(0, 0)) + r.scroll.Move(fyne.NewPos(0, inputBorder)) + r.scroll.Resize(entrySize) + r.scroll.Show() + + for i, o := range r.objects { + if o == content { + r.objects[i] = r.scroll + break + } + } + } + r.entry.updateCursorAndSelection() + + v := fyne.CurrentApp().Settings().ThemeVariant() + r.box.FillColor = th.Color(theme.ColorNameInputBackground, v) + r.box.CornerRadius = th.Size(theme.SizeNameInputRadius) + r.border.CornerRadius = r.box.CornerRadius + if focusedAppearance { + r.border.StrokeColor = th.Color(theme.ColorNamePrimary, v) + } else { + if r.entry.Disabled() { + r.border.StrokeColor = th.Color(theme.ColorNameDisabled, v) + } else { + r.border.StrokeColor = th.Color(theme.ColorNameInputBorder, v) + } + } + if r.entry.ActionItem != nil { + r.entry.ActionItem.Refresh() + } + + if r.entry.Validator != nil || r.entry.AlwaysShowValidationError { + if !r.entry.focused && !r.entry.Disabled() && (r.entry.dirty || r.entry.AlwaysShowValidationError) && r.entry.validationError != nil { + r.border.StrokeColor = th.Color(theme.ColorNameError, v) + } + r.ensureValidationSetup() + r.entry.validationStatus.Refresh() + } else if r.entry.validationStatus != nil { + r.entry.validationStatus.Hide() + } + + if r.entry.Icon != nil { + r.icon.Resource = r.entry.Icon + r.icon.Refresh() + r.icon.Show() + } else { + r.icon.Hide() + } + + r.entry.sel.Hidden = !r.entry.focused + + cache.Renderer(r.entry.content).Refresh() + canvas.Refresh(r.entry.super()) +} + +func (r *entryRenderer) ensureValidationSetup() { + if r.entry.validationStatus == nil { + r.entry.validationStatus = newValidationStatus(r.entry) + r.objects = append(r.objects, r.entry.validationStatus) + r.Layout(r.entry.Size()) + + r.entry.validate() + r.Refresh() + } +} + +var _ fyne.Widget = (*entryContent)(nil) + +type entryContent struct { + BaseWidget + + entry *Entry + scroll *widget.Scroll +} + +func (e *entryContent) CreateRenderer() fyne.WidgetRenderer { + e.ExtendBaseWidget(e) + + provider := e.entry.textProvider() + placeholder := e.entry.placeholderProvider() + if provider.len() != 0 { + placeholder.Hide() + } + objects := []fyne.CanvasObject{placeholder, provider, e.entry.cursorAnim.cursor} + + r := &entryContentRenderer{ + e.entry.cursorAnim.cursor, objects, + provider, placeholder, e, + } + r.updateScrollDirections() + r.Layout(e.Size()) + return r +} + +// DragEnd is called at end of a drag event. +func (e *entryContent) DragEnd() { + // we need to propagate the focus, top level widget handles focus APIs + e.entry.requestFocus() + + e.entry.DragEnd() +} + +// Dragged is called when the pointer moves while a button is held down. +// It updates the selection accordingly. +func (e *entryContent) Dragged(d *fyne.DragEvent) { + e.entry.Dragged(d) +} + +var _ fyne.WidgetRenderer = (*entryContentRenderer)(nil) + +type entryContentRenderer struct { + cursor *canvas.Rectangle + objects []fyne.CanvasObject + + provider, placeholder *RichText + content *entryContent +} + +func (r *entryContentRenderer) Destroy() { + r.content.entry.cursorAnim.stop() +} + +func (r *entryContentRenderer) Layout(size fyne.Size) { + r.provider.Resize(size) + r.placeholder.Resize(size) +} + +func (r *entryContentRenderer) MinSize() fyne.Size { + r.content.Theme() // setup theme cache before locking + minSize := r.content.entry.placeholderProvider().MinSize() + + if r.content.entry.textProvider().len() > 0 { + minSize = r.content.entry.text.MinSize() + } + + return minSize +} + +func (r *entryContentRenderer) Objects() []fyne.CanvasObject { + // Objects are generated dynamically force selection rectangles to appear underneath the text + if r.content.entry.sel.selecting { + return append([]fyne.CanvasObject{r.content.entry.sel}, r.objects...) + } + return r.objects +} + +func (r *entryContentRenderer) Refresh() { + provider := r.content.entry.textProvider() + placeholder := r.content.entry.placeholderProvider() + focused := r.content.entry.focused + focusedAppearance := focused && !r.content.entry.Disabled() + r.updateScrollDirections() + + if provider.len() == 0 { + placeholder.Show() + } else if placeholder.Visible() { + placeholder.Hide() + } + + th := r.content.entry.Theme() + if focusedAppearance { + settings := fyne.CurrentApp().Settings() + if settings.ShowAnimations() { + r.content.entry.cursorAnim.start() + } else { + r.cursor.FillColor = th.Color(theme.ColorNamePrimary, settings.ThemeVariant()) + } + r.cursor.Show() + } else { + r.content.entry.cursorAnim.stop() + r.cursor.Hide() + } + r.moveCursor() + + canvas.Refresh(r.content) +} + +func (r *entryContentRenderer) ensureCursorVisible() { + th := r.content.entry.Theme() + lineSpace := th.Size(theme.SizeNameLineSpacing) + + letter := fyne.MeasureText("e", th.Size(theme.SizeNameText), r.content.entry.TextStyle) + padX := letter.Width*2 + lineSpace + padY := letter.Height - lineSpace + cx := r.cursor.Position().X + cy := r.cursor.Position().Y + cx1 := cx - padX + cy1 := cy - padY + cx2 := cx + r.cursor.Size().Width + padX + cy2 := cy + r.cursor.Size().Height + padY + offset := r.content.scroll.Offset + size := r.content.scroll.Size() + + if offset.X <= cx1 && cx2 < offset.X+size.Width && + offset.Y <= cy1 && cy2 < offset.Y+size.Height { + return + } + + move := fyne.NewDelta(0, 0) + if cx1 < offset.X { + move.DX -= offset.X - cx1 + } else if cx2 >= offset.X+size.Width { + move.DX += cx2 - (offset.X + size.Width) + } + if cy1 < offset.Y { + move.DY -= offset.Y - cy1 + } else if cy2 >= offset.Y+size.Height { + move.DY += cy2 - (offset.Y + size.Height) + } + if r.content.scroll.Content != nil { + r.content.scroll.ScrollToOffset(r.content.scroll.Offset.Add(move)) + } +} + +func (r *entryContentRenderer) moveCursor() { + // build r.selection[] if the user has made a selection + r.content.entry.sel.Refresh() + + th := r.content.entry.Theme() + textSize := th.Size(theme.SizeNameText) + inputBorder := th.Size(theme.SizeNameInputBorder) + + lineHeight := r.content.entry.text.charMinSize(r.content.entry.Password, r.content.entry.TextStyle, textSize).Height + r.cursor.Resize(fyne.NewSize(inputBorder, lineHeight)) + r.cursor.Move(r.content.entry.CursorPosition()) + + callback := r.content.entry.OnCursorChanged + r.ensureCursorVisible() + + if callback != nil { + callback() + } +} + +func (r *entryContentRenderer) updateScrollDirections() { + if r.content.scroll == nil { // not scrolling + return + } + + switch r.content.entry.Wrapping { + case fyne.TextWrapOff: + r.content.scroll.Direction = r.content.entry.Scroll + case fyne.TextWrap(fyne.TextTruncateClip): // this is now the default - but we scroll + r.content.scroll.Direction = widget.ScrollBoth + default: // fyne.TextWrapBreak, fyne.TextWrapWord + r.content.scroll.Direction = widget.ScrollVerticalOnly + } +} + +// getTextWhitespaceRegion returns the start/end markers for selection highlight on starting from col +// and expanding to the start and end of the whitespace or text underneath the specified position. +// Pass `true` for `expand` if you want whitespace selection to extend to the neighboring words. +func getTextWhitespaceRegion(row []rune, col int, expand bool) (int, int) { + if len(row) == 0 || col < 0 { + return -1, -1 + } + + // If the click position exceeds the length of text then snap it to the end + if col >= len(row) { + col = len(row) - 1 + } + + // maps: " fi-sh 日本語本語日 \t " + // into: " -- -- ------ " + space := func(r rune) rune { + // If this rune is a typical word separator then classify it as whitespace + if isWordSeparator(r) { + return ' ' + } + return '-' + } + toks := strings.Map(space, string(row)) + c := byte(' ') + + startCheck := col + endCheck := col + if expand { + if col > 0 && toks[col-1] == ' ' { // ignore the prior whitespace then count + startCheck = strings.LastIndexByte(toks[:startCheck], '-') + if startCheck == -1 { + startCheck = 0 + } + } + if toks[col] == ' ' { // ignore the current whitespace then count + endCheck = col + strings.IndexByte(toks[endCheck:], '-') + } + } else if toks[col] == ' ' { + c = byte('-') + } + + // LastIndexByte + 1 ensures that the position of the unwanted character ' ' is excluded + // +1 also has the added side effect whereby if ' ' isn't found then -1 is snapped to 0 + start := strings.LastIndexByte(toks[:startCheck], c) + 1 + + // IndexByte will find the position of the next unwanted character, this is to be the end + // marker for the selection + end := -1 + if endCheck != -1 { + end = strings.IndexByte(toks[endCheck:], c) + } + + if end == -1 { + end = len(toks) // snap end to len(toks) if it results in -1 + } else { + end += endCheck // otherwise include the text slice position + } + return start, end +} + +func isWordSeparator(r rune) bool { + return unicode.IsSpace(r) || + strings.ContainsRune("`~!@#$%^&*()-=+[{]}\\|;:'\",.<>/?", r) +} + +// entryUndoAction represents a single user action that can be undone +type entryUndoAction interface { + Undo(string) string + Redo(string) string +} + +// entryMergeableUndoAction is like entryUndoAction, but the undoStack +// can try to merge it with the next action (see TryMerge). +// This is useful because it allows grouping together actions like +// entering every single characters in a word. We don't want to have to +// undo every single character addition. +type entryMergeableUndoAction interface { + entryUndoAction + // TryMerge attempts to merge the current action + // with the next action. It returns true if successful. + // If it fails, the undoStack will simply add the next + // item without merging. + TryMerge(next entryMergeableUndoAction) bool +} + +// Declare conformity with entryMergeableUndoAction interface +var _ entryMergeableUndoAction = (*entryModifyAction)(nil) + +// entryModifyAction implements entryMergeableUndoAction. +// It represents the insertion/deletion of a single string at a +// position (e.g. "Hello" => "Hello, world", or "Hello" => "He"). +type entryModifyAction struct { + // Delete is true if this action deletes Text, and false if it inserts Text + Delete bool + // Position represents the start position of Text + Position int + // Text is the text that is inserted or deleted at Position + Text []rune +} + +func (i *entryModifyAction) Undo(s string) string { + if i.Delete { + return i.add(s) + } else { + return i.sub(s) + } +} + +func (i *entryModifyAction) Redo(s string) string { + if i.Delete { + return i.sub(s) + } else { + return i.add(s) + } +} + +// Inserts Text +func (i *entryModifyAction) add(s string) string { + runes := []rune(s) + return string(runes[:i.Position]) + string(i.Text) + string(runes[i.Position:]) +} + +// Deletes Text +func (i *entryModifyAction) sub(s string) string { + runes := []rune(s) + return string(runes[:i.Position]) + string(runes[i.Position+len(i.Text):]) +} + +func (i *entryModifyAction) TryMerge(other entryMergeableUndoAction) bool { + if other, ok := other.(*entryModifyAction); ok { + // Don't merge two different types of modifyAction + if i.Delete != other.Delete { + return false + } + + // Don't merge two separate words + wordSeparators := func(s []rune) (num int, onlyWordSeparators bool) { + onlyWordSeparators = true + for _, r := range s { + if isWordSeparator(r) { + num++ + onlyWordSeparators = false + } + } + return num, onlyWordSeparators + } + selfNumWS, _ := wordSeparators(i.Text) + otherNumWS, otherOnlyWS := wordSeparators(other.Text) + if !((selfNumWS == 0 && otherNumWS == 0) || + (selfNumWS > 0 && otherOnlyWS)) { + return false + } + + if i.Delete { + if i.Position == other.Position+len(other.Text) { + i.Position = other.Position + i.Text = append(other.Text, i.Text...) + return true + } + } else { + if i.Position+len(i.Text) == other.Position { + i.Text = append(i.Text, other.Text...) + return true + } + } + return false + } + return false +} + +// entryUndoStack stores the information necessary for textual undo/redo functionality. +type entryUndoStack struct { + // items is the stack for storing the history of user actions. + items []entryUndoAction + // index is the size of the current effective undo stack. + // items[index-1] and below are the possible undo actions. + // items[index] and above are the possible redo actions. + index int +} + +// Applies the undo action to s and returns the result along with the action performed +func (u *entryUndoStack) Undo(s string) (newS string, action entryUndoAction) { + if !u.CanUndo() { + return s, nil + } + u.index-- + action = u.items[u.index] + return action.Undo(s), action +} + +// Applies the redo action to s and returns the result along with the action performed +func (u *entryUndoStack) Redo(s string) (newS string, action entryUndoAction) { + if !u.CanRedo() { + return s, nil + } + action = u.items[u.index] + res := action.Redo(s) + u.index++ + return res, action +} + +// Returns true if an undo action is available +func (u *entryUndoStack) CanUndo() bool { + return u.index != 0 +} + +// Returns true if an redo action is available +func (u *entryUndoStack) CanRedo() bool { + return u.index != len(u.items) +} + +// Adds the action to the stack, which can later be undone by calling Undo() +func (u *entryUndoStack) Add(a entryUndoAction) { + u.items = u.items[:u.index] + u.items = append(u.items, a) + u.index++ +} + +// Tries to merge the action with the last item on the undo stack. +// If it can't be merged, it calls Add(). +func (u *entryUndoStack) MergeOrAdd(a entryUndoAction) { + u.items = u.items[:u.index] + if u.index == 0 { + u.Add(a) + return + } + ma, ok := a.(entryMergeableUndoAction) + if !ok { + u.Add(a) + return + } + mprev, ok := u.items[u.index-1].(entryMergeableUndoAction) + if !ok { + u.Add(a) + return + } + if !mprev.TryMerge(ma) { + u.Add(a) + return + } +} + +// Removes all items from the undo stack +func (u *entryUndoStack) Clear() { + u.items = nil + u.index = 0 +} diff --git a/vendor/fyne.io/fyne/v2/widget/entry_cursor_anim.go b/vendor/fyne.io/fyne/v2/widget/entry_cursor_anim.go new file mode 100644 index 0000000..1645167 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/entry_cursor_anim.go @@ -0,0 +1,123 @@ +package widget + +import ( + "image/color" + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + col "fyne.io/fyne/v2/internal/color" + "fyne.io/fyne/v2/theme" +) + +var timeNow = time.Now // used in tests + +const ( + cursorInterruptTime = 300 * time.Millisecond + cursorFadeAlpha = uint8(0x16) + cursorFadeRatio = 0.1 +) + +type entryCursorAnimation struct { + cursor *canvas.Rectangle + anim *fyne.Animation + lastInterruptTime time.Time +} + +func newEntryCursorAnimation(cursor *canvas.Rectangle) *entryCursorAnimation { + return &entryCursorAnimation{cursor: cursor} +} + +// creates fyne animation +func (a *entryCursorAnimation) createAnim(inverted bool) *fyne.Animation { + cursorOpaque := theme.Color(theme.ColorNamePrimary) + ri, gi, bi, ai := col.ToNRGBA(cursorOpaque) + r := uint8(ri >> 8) + g := uint8(gi >> 8) + b := uint8(bi >> 8) + endA := uint8(ai >> 8) + startA := cursorFadeAlpha + cursorDim := color.NRGBA{R: r, G: g, B: b, A: cursorFadeAlpha} + if inverted { + a.cursor.FillColor = cursorOpaque + startA, endA = endA, startA + } else { + a.cursor.FillColor = cursorDim + } + + deltaA := endA - startA + fadeStart := float32(0.5 - cursorFadeRatio) + fadeStop := float32(0.5 + cursorFadeRatio) + + interrupted := false + anim := fyne.NewAnimation(time.Second/2, func(f float32) { + shouldInterrupt := timeNow().Sub(a.lastInterruptTime) <= cursorInterruptTime + if shouldInterrupt { + if !interrupted { + a.cursor.FillColor = cursorOpaque + a.cursor.Refresh() + interrupted = true + } + return + } + if interrupted { + a.anim.Stop() + if !inverted { + a.anim = a.createAnim(true) + } + interrupted = false + canStart := a.anim != nil + if canStart { + a.anim.Start() + } + return + } + + alpha := uint8(0) + if f < fadeStart { + if _, _, _, al := a.cursor.FillColor.RGBA(); uint8(al>>8) == cursorFadeAlpha { + return + } + + a.cursor.FillColor = cursorDim + } else if f >= fadeStop { + if _, _, _, al := a.cursor.FillColor.RGBA(); al == 0xffff { + return + } + + a.cursor.FillColor = cursorOpaque + } else { + fade := (f + cursorFadeRatio - 0.5) * (1 / (cursorFadeRatio * 2)) + alpha = uint8(float32(deltaA) * fade) + a.cursor.FillColor = color.NRGBA{R: r, G: g, B: b, A: alpha} + } + + a.cursor.Refresh() + }) + + anim.RepeatCount = fyne.AnimationRepeatForever + anim.AutoReverse = true + return anim +} + +// starts cursor animation. +func (a *entryCursorAnimation) start() { + isStopped := a.anim == nil + if isStopped { + a.anim = a.createAnim(false) + a.anim.Start() + } +} + +// temporarily stops the animation by "cursorInterruptTime". +func (a *entryCursorAnimation) interrupt() { + a.lastInterruptTime = timeNow() +} + +// stops cursor animation. +func (a *entryCursorAnimation) stop() { + if a.anim != nil { + a.anim.Stop() + a.anim = nil + } +} diff --git a/vendor/fyne.io/fyne/v2/widget/entry_password.go b/vendor/fyne.io/fyne/v2/widget/entry_password.go new file mode 100644 index 0000000..d3c92ef --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/entry_password.go @@ -0,0 +1,87 @@ +package widget + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/driver/desktop" + "fyne.io/fyne/v2/theme" +) + +var ( + _ desktop.Cursorable = (*passwordRevealer)(nil) + _ fyne.Tappable = (*passwordRevealer)(nil) + _ fyne.Widget = (*passwordRevealer)(nil) +) + +type passwordRevealer struct { + BaseWidget + + icon *canvas.Image + entry *Entry +} + +func newPasswordRevealer(e *Entry) *passwordRevealer { + th := e.Theme() + pr := &passwordRevealer{ + icon: canvas.NewImageFromResource(th.Icon(theme.IconNameVisibilityOff)), + entry: e, + } + pr.ExtendBaseWidget(pr) + return pr +} + +func (r *passwordRevealer) CreateRenderer() fyne.WidgetRenderer { + return &passwordRevealerRenderer{ + WidgetRenderer: NewSimpleRenderer(r.icon), + icon: r.icon, + entry: r.entry, + } +} + +func (r *passwordRevealer) Cursor() desktop.Cursor { + return desktop.DefaultCursor +} + +func (r *passwordRevealer) Tapped(*fyne.PointEvent) { + if r.entry.Disabled() { + return + } + + r.entry.setFieldsAndRefresh(func() { + r.entry.Password = !r.entry.Password + }) + fyne.CurrentApp().Driver().CanvasForObject(r).Focus(r.entry.super().(fyne.Focusable)) +} + +var _ fyne.WidgetRenderer = (*passwordRevealerRenderer)(nil) + +type passwordRevealerRenderer struct { + fyne.WidgetRenderer + entry *Entry + icon *canvas.Image +} + +func (r *passwordRevealerRenderer) Layout(size fyne.Size) { + iconSize := r.entry.Theme().Size(theme.SizeNameInlineIcon) + r.icon.Resize(fyne.NewSquareSize(iconSize)) + r.icon.Move(fyne.NewPos((size.Width-iconSize)/2, (size.Height-iconSize)/2)) +} + +func (r *passwordRevealerRenderer) MinSize() fyne.Size { + iconSize := r.entry.Theme().Size(theme.SizeNameInlineIcon) + return fyne.NewSquareSize(iconSize + r.entry.Theme().Size(theme.SizeNameInnerPadding)*2) +} + +func (r *passwordRevealerRenderer) Refresh() { + th := r.entry.Theme() + if !r.entry.Password { + r.icon.Resource = th.Icon(theme.IconNameVisibility) + } else { + r.icon.Resource = th.Icon(theme.IconNameVisibilityOff) + } + + if r.entry.Disabled() { + r.icon.Resource = theme.NewDisabledResource(r.icon.Resource) + } + r.icon.Refresh() +} diff --git a/vendor/fyne.io/fyne/v2/widget/entry_validation.go b/vendor/fyne.io/fyne/v2/widget/entry_validation.go new file mode 100644 index 0000000..8f3285b --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/entry_validation.go @@ -0,0 +1,136 @@ +package widget + +import ( + "errors" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/theme" +) + +var _ fyne.Validatable = (*Entry)(nil) + +// Validate validates the current text in the widget. +func (e *Entry) Validate() error { + if e.Validator == nil { + return nil + } + + err := e.Validator(e.Text) + e.SetValidationError(err) + return err +} + +// validate works like Validate but only updates the internal state and does not refresh. +func (e *Entry) validate() { + if e.Validator == nil { + return + } + + err := e.Validator(e.Text) + e.setValidationError(err) +} + +// SetOnValidationChanged is intended for parent widgets or containers to hook into the validation. +// The function might be overwritten by a parent that cares about child validation (e.g. widget.Form). +func (e *Entry) SetOnValidationChanged(callback func(error)) { + e.onValidationChanged = callback +} + +// SetValidationError manually updates the validation status until the next input change. +func (e *Entry) SetValidationError(err error) { + if e.Validator == nil && !e.AlwaysShowValidationError { + return + } + + if !e.setValidationError(err) { + return + } + + e.Refresh() +} + +// setValidationError sets the validation error and returns a bool to indicate if it changes. +// It assumes that the widget has a validator. +func (e *Entry) setValidationError(err error) bool { + if err == nil && e.validationError == nil { + return false + } + if errors.Is(err, e.validationError) { + return false + } + + changed := e.validationError != err + e.validationError = err + + if e.onValidationChanged != nil && changed { + e.onValidationChanged(err) + } + + return true +} + +var _ fyne.Widget = (*validationStatus)(nil) + +type validationStatus struct { + BaseWidget + entry *Entry +} + +func newValidationStatus(e *Entry) *validationStatus { + rs := &validationStatus{ + entry: e, + } + + rs.ExtendBaseWidget(rs) + return rs +} + +func (r *validationStatus) CreateRenderer() fyne.WidgetRenderer { + icon := &canvas.Image{} + icon.Hide() + return &validationStatusRenderer{ + WidgetRenderer: NewSimpleRenderer(icon), + icon: icon, + entry: r.entry, + } +} + +var _ fyne.WidgetRenderer = (*validationStatusRenderer)(nil) + +type validationStatusRenderer struct { + fyne.WidgetRenderer + entry *Entry + icon *canvas.Image +} + +func (r *validationStatusRenderer) Layout(size fyne.Size) { + iconSize := r.entry.Theme().Size(theme.SizeNameInlineIcon) + r.icon.Resize(fyne.NewSquareSize(iconSize)) + r.icon.Move(fyne.NewPos((size.Width-iconSize)/2, (size.Height-iconSize)/2)) +} + +func (r *validationStatusRenderer) MinSize() fyne.Size { + iconSize := r.entry.Theme().Size(theme.SizeNameInlineIcon) + return fyne.NewSquareSize(iconSize) +} + +func (r *validationStatusRenderer) Refresh() { + th := r.entry.Theme() + if r.entry.Disabled() { + r.icon.Hide() + return + } + + if r.entry.validationError == nil && r.entry.Text != "" && r.entry.Validator != nil { + r.icon.Resource = th.Icon(theme.IconNameConfirm) + r.icon.Show() + } else if r.entry.validationError != nil && !r.entry.focused && (r.entry.dirty || r.entry.AlwaysShowValidationError) { + r.icon.Resource = theme.NewErrorThemedResource(th.Icon(theme.IconNameError)) + r.icon.Show() + } else { + r.icon.Hide() + } + + r.icon.Refresh() +} diff --git a/vendor/fyne.io/fyne/v2/widget/fileicon.go b/vendor/fyne.io/fyne/v2/widget/fileicon.go new file mode 100644 index 0000000..15524dd --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/fileicon.go @@ -0,0 +1,212 @@ +package widget + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/internal/repository/mime" + "fyne.io/fyne/v2/internal/widget" + "fyne.io/fyne/v2/storage" + "fyne.io/fyne/v2/theme" +) + +const ( + ratioDown = 0.45 + ratioTextSize = 0.22 +) + +// FileIcon is an adaption of widget.Icon for showing files and folders +// +// Since: 1.4 +type FileIcon struct { + BaseWidget + + // Deprecated: Selection is now handled externally. + Selected bool + URI fyne.URI + + resource fyne.Resource + extension string +} + +// NewFileIcon takes a filepath and creates an icon with an overlaid label using the detected mimetype and extension +// +// Since: 1.4 +func NewFileIcon(uri fyne.URI) *FileIcon { + i := &FileIcon{URI: uri} + i.ExtendBaseWidget(i) + return i +} + +// SetURI changes the URI and makes the icon reflect a different file +func (i *FileIcon) SetURI(uri fyne.URI) { + i.URI = uri + i.Refresh() +} + +// must be called with i.propertyLock RLocked +func (i *FileIcon) setURI(uri fyne.URI) { + if uri == nil { + i.resource = i.Theme().Icon(theme.IconNameFile) + return + } + + i.URI = uri + i.resource = i.lookupIcon(i.URI) + i.extension = trimmedExtension(uri) +} + +// MinSize returns the size that this widget should not shrink below +func (i *FileIcon) MinSize() fyne.Size { + i.ExtendBaseWidget(i) + return i.BaseWidget.MinSize() +} + +// CreateRenderer is a private method to Fyne which links this widget to its renderer +func (i *FileIcon) CreateRenderer() fyne.WidgetRenderer { + th := i.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + i.ExtendBaseWidget(i) + i.setURI(i.URI) + + // TODO remove background when `SetSelected` is gone. + background := canvas.NewRectangle(th.Color(theme.ColorNameSelection, v)) + background.Hide() + + s := &fileIconRenderer{file: i, background: background} + s.img = canvas.NewImageFromResource(s.file.resource) + s.img.FillMode = canvas.ImageFillContain + s.ext = canvas.NewText(s.file.extension, th.Color(theme.ColorNameBackground, v)) + s.ext.Alignment = fyne.TextAlignCenter + + s.SetObjects([]fyne.CanvasObject{s.background, s.img, s.ext}) + + return s +} + +// SetSelected makes the file look like it is selected. +// +// Deprecated: Selection is now handled externally. +func (i *FileIcon) SetSelected(selected bool) { + i.Selected = selected + i.Refresh() +} + +// must be called with i.propertyLock RLocked +func (i *FileIcon) lookupIcon(uri fyne.URI) fyne.Resource { + if icon, ok := uri.(fyne.URIWithIcon); ok { + return icon.Icon() + } + if i.isDir(uri) { + return theme.FolderIcon() + } + + th := i.Theme() + mainMimeType, _ := mime.Split(uri.MimeType()) + switch mainMimeType { + case "application": + return th.Icon(theme.IconNameFileApplication) + case "audio": + return th.Icon(theme.IconNameFileAudio) + case "image": + return th.Icon(theme.IconNameFileImage) + case "text": + return th.Icon(theme.IconNameFileText) + case "video": + return th.Icon(theme.IconNameFileVideo) + default: + return th.Icon(theme.IconNameFile) + } +} + +func (i *FileIcon) isDir(uri fyne.URI) bool { + if _, ok := uri.(fyne.ListableURI); ok { + return true + } + + listable, err := storage.ListerForURI(uri) + if err != nil { + return false + } + + i.URI = listable // Avoid having to call storage.ListerForURI(uri) the next time. + return true +} + +type fileIconRenderer struct { + widget.BaseRenderer + + file *FileIcon + + background *canvas.Rectangle + ext *canvas.Text + img *canvas.Image +} + +func (s *fileIconRenderer) MinSize() fyne.Size { + th := s.file.Theme() + return fyne.NewSquareSize(th.Size(theme.SizeNameInlineIcon)) +} + +func (s *fileIconRenderer) Layout(size fyne.Size) { + isize := fyne.Min(size.Width, size.Height) + + xoff := float32(0) + yoff := (size.Height - isize) / 2 + + if size.Width > size.Height { + xoff = (size.Width - isize) / 2 + } + yoff += isize * ratioDown + + oldSize := s.ext.TextSize + s.ext.TextSize = float32(int(isize * ratioTextSize)) + s.ext.Resize(fyne.NewSize(isize, s.ext.MinSize().Height)) + s.ext.Move(fyne.NewPos(xoff, yoff)) + if oldSize != s.ext.TextSize { + s.ext.Refresh() + } + + s.Objects()[0].Resize(size) + s.Objects()[1].Resize(size) +} + +func (s *fileIconRenderer) Refresh() { + th := s.file.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + s.file.setURI(s.file.URI) + + if s.file.Selected { + s.background.Show() + s.ext.Color = th.Color(theme.ColorNameSelection, v) + if _, ok := s.img.Resource.(*theme.InvertedThemedResource); !ok { + s.img.Resource = theme.NewInvertedThemedResource(s.img.Resource) + } + } else { + s.background.Hide() + s.ext.Color = th.Color(theme.ColorNameBackground, v) + if res, ok := s.img.Resource.(*theme.InvertedThemedResource); ok { + s.img.Resource = res.Original() + } + } + + if s.img.Resource != s.file.resource { + s.img.Resource = s.file.resource + s.img.Refresh() + } + if s.ext.Text != s.file.extension { + s.ext.Text = s.file.extension + s.ext.Refresh() + } + + canvas.Refresh(s.file.super()) +} + +func trimmedExtension(uri fyne.URI) string { + ext := uri.Extension() + if len(ext) > 5 { + ext = ext[:5] + } + return ext +} diff --git a/vendor/fyne.io/fyne/v2/widget/form.go b/vendor/fyne.io/fyne/v2/widget/form.go new file mode 100644 index 0000000..1d43528 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/form.go @@ -0,0 +1,497 @@ +package widget + +import ( + "errors" + "reflect" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/internal/cache" + "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/theme" +) + +// errFormItemInitialState defines the error if the initial validation for a FormItem result +// in an error +var errFormItemInitialState = errors.New("widget.FormItem initial state error") + +// FormItem provides the details for a row in a form +type FormItem struct { + Text string + Widget fyne.CanvasObject + + // Since: 2.0 + HintText string + + validationError error + invalid bool + helperOutput *canvas.Text + wasFocused bool +} + +// NewFormItem creates a new form item with the specified label text and input widget +func NewFormItem(text string, widget fyne.CanvasObject) *FormItem { + return &FormItem{Text: text, Widget: widget} +} + +var _ fyne.Validatable = (*Form)(nil) + +// Form widget is two column grid where each row has a label and a widget (usually an input). +// The last row of the grid will contain the appropriate form control buttons if any should be shown. +// Setting OnSubmit will set the submit button to be visible and call back the function when tapped. +// Setting OnCancel will do the same for a cancel button. +// If you change OnSubmit/OnCancel after the form is created and rendered, you need to call +// Refresh() to update the form with the correct buttons. +// Setting OnSubmit/OnCancel to nil will remove the buttons. +type Form struct { + BaseWidget + + Items []*FormItem + OnSubmit func() `json:"-"` + OnCancel func() `json:"-"` + SubmitText string + CancelText string + + // Orientation allows a form to be vertical (a single column), horizontal (default, label then input) + // or to adapt according to the orientation of the mobile device (adaptive). + // + // Since: 2.5 + Orientation Orientation + + itemGrid *fyne.Container + buttonBox *fyne.Container + cancelButton *Button + submitButton *Button + + disabled bool + + onValidationChanged func(error) + validationError error +} + +// Append adds a new row to the form, using the text as a label next to the specified Widget +func (f *Form) Append(text string, widget fyne.CanvasObject) { + item := &FormItem{Text: text, Widget: widget} + f.AppendItem(item) +} + +// AppendItem adds the specified row to the end of the Form +func (f *Form) AppendItem(item *FormItem) { + f.ExtendBaseWidget(f) // could be called before render + + f.Items = append(f.Items, item) + if f.itemGrid != nil { + f.itemGrid.Add(f.createLabel(item.Text)) + f.itemGrid.Add(f.createInput(item)) + f.setUpValidation(item.Widget, len(f.Items)-1) + } + + f.Refresh() +} + +// MinSize returns the size that this widget should not shrink below +func (f *Form) MinSize() fyne.Size { + f.ExtendBaseWidget(f) + return f.BaseWidget.MinSize() +} + +// Refresh updates the widget state when requested. +func (f *Form) Refresh() { + f.ExtendBaseWidget(f) + cache.Renderer(f.super()) // we are about to make changes to renderer created content... not great! + f.ensureRenderItems() + f.updateButtons() + f.updateLabels() + + if f.isVertical() { + f.itemGrid.Layout = layout.NewVBoxLayout() + } else { + f.itemGrid.Layout = layout.NewFormLayout() + } + + f.BaseWidget.Refresh() + canvas.Refresh(f.super()) // refresh ourselves for BG color - the above updates the content +} + +// Enable enables submitting this form. +// +// Since: 2.1 +func (f *Form) Enable() { + f.disabled = false + f.cancelButton.Enable() + f.checkValidation(nil) // as the form may be invalid +} + +// Disable disables submitting this form. +// +// Since: 2.1 +func (f *Form) Disable() { + f.disabled = true + f.submitButton.Disable() + f.cancelButton.Disable() +} + +// Disabled returns whether submitting the form is disabled. +// Note that, if the form fails validation, the submit button may be +// disabled even if this method returns true. +// +// Since: 2.1 +func (f *Form) Disabled() bool { + return f.disabled +} + +// SetOnValidationChanged is intended for parent widgets or containers to hook into the validation. +// The function might be overwritten by a parent that cares about child validation (e.g. widget.Form) +func (f *Form) SetOnValidationChanged(callback func(error)) { + f.onValidationChanged = callback +} + +// Validate validates the entire form and returns the first error that is encountered. +func (f *Form) Validate() error { + for _, item := range f.Items { + if w, ok := item.Widget.(fyne.Validatable); ok { + if err := w.Validate(); err != nil { + return err + } + } + } + return nil +} + +func (f *Form) createInput(item *FormItem) fyne.CanvasObject { + _, ok := item.Widget.(fyne.Validatable) + if item.HintText == "" { + if !ok { + return item.Widget + } + if !f.itemWidgetHasValidator(item.Widget) { // we don't have validation + return item.Widget + } + } + + th := f.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + text := canvas.NewText(item.HintText, th.Color(theme.ColorNamePlaceHolder, v)) + text.TextSize = th.Size(theme.SizeNameCaptionText) + item.helperOutput = text + f.updateHelperText(item) + textContainer := &fyne.Container{Objects: []fyne.CanvasObject{text}} + return &fyne.Container{Layout: formItemLayout{form: f}, Objects: []fyne.CanvasObject{item.Widget, textContainer}} +} + +func (f *Form) itemWidgetHasValidator(w fyne.CanvasObject) bool { + value := reflect.ValueOf(w).Elem() + validatorField := value.FieldByName("Validator") + if validatorField == (reflect.Value{}) { + return false + } + validator, ok := validatorField.Interface().(fyne.StringValidator) + if !ok { + return false + } + return validator != nil +} + +func (f *Form) createLabel(text string) fyne.CanvasObject { + th := f.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + label := &canvas.Text{ + Text: text, + Alignment: fyne.TextAlignTrailing, + Color: th.Color(theme.ColorNameForeground, v), + TextSize: th.Size(theme.SizeNameText), + TextStyle: fyne.TextStyle{Bold: true}, + } + if f.isVertical() { + label.Alignment = fyne.TextAlignLeading + } + + return &fyne.Container{Layout: &formLabelLayout{form: f}, Objects: []fyne.CanvasObject{label}} +} + +func (f *Form) updateButtons() { + if f.CancelText == "" { + f.CancelText = "Cancel" + } + if f.SubmitText == "" { + f.SubmitText = "Submit" + } + + // set visibility on the buttons + if f.OnCancel == nil { + f.cancelButton.Hide() + } else { + f.cancelButton.SetText(f.CancelText) + f.cancelButton.OnTapped = f.OnCancel + f.cancelButton.Show() + } + if f.OnSubmit == nil { + f.submitButton.Hide() + } else { + f.submitButton.SetText(f.SubmitText) + f.submitButton.OnTapped = f.OnSubmit + f.submitButton.Show() + } + if f.OnCancel == nil && f.OnSubmit == nil { + f.buttonBox.Hide() + } else { + f.buttonBox.Show() + } +} + +func (f *Form) checkValidation(err error) { + if err != nil { + f.submitButton.Disable() + return + } + + for _, item := range f.Items { + if item.invalid { + f.submitButton.Disable() + return + } + } + + if !f.disabled { + f.submitButton.Enable() + } +} + +func (f *Form) ensureRenderItems() { + done := len(f.itemGrid.Objects) / 2 + if done >= len(f.Items) { + f.itemGrid.Objects = f.itemGrid.Objects[0 : len(f.Items)*2] + return + } + + adding := len(f.Items) - done + objects := make([]fyne.CanvasObject, adding*2) + off := 0 + for i, item := range f.Items { + if i < done { + continue + } + + objects[off] = f.createLabel(item.Text) + off++ + f.setUpValidation(item.Widget, i) + objects[off] = f.createInput(item) + off++ + } + f.itemGrid.Objects = append(f.itemGrid.Objects, objects...) +} + +func (f *Form) isVertical() bool { + if f.Orientation == Vertical { + return true + } else if f.Orientation == Horizontal { + return false + } + + dev := fyne.CurrentDevice() + if dev.IsMobile() { + orient := dev.Orientation() + return orient == fyne.OrientationVertical || orient == fyne.OrientationVerticalUpsideDown + } + + return false +} + +func (f *Form) setUpValidation(widget fyne.CanvasObject, i int) { + updateValidation := func(err error) { + if err == errFormItemInitialState { + return + } + f.Items[i].validationError = err + f.Items[i].invalid = err != nil + f.setValidationError(err) + f.checkValidation(err) + f.updateHelperText(f.Items[i]) + } + if w, ok := widget.(fyne.Validatable); ok { + f.Items[i].invalid = w.Validate() != nil + if e, ok := w.(*Entry); ok { + e.onFocusChanged = func(bool) { + updateValidation(e.validationError) + } + if e.Validator != nil && f.Items[i].invalid { + // set initial state error to guarantee next error (if triggers) is always different + e.SetValidationError(errFormItemInitialState) + } + } + w.SetOnValidationChanged(updateValidation) + } +} + +func (f *Form) setValidationError(err error) { + if err == nil && f.validationError == nil { + return + } + + if !errors.Is(err, f.validationError) { + if err == nil { + for _, item := range f.Items { + if item.invalid { + err = item.validationError + break + } + } + } + f.validationError = err + + if f.onValidationChanged != nil { + f.onValidationChanged(err) + } + } +} + +func (f *Form) updateHelperText(item *FormItem) { + th := f.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + if item.helperOutput == nil { + return // testing probably, either way not rendered yet + } + showHintIfError := false + if e, ok := item.Widget.(*Entry); ok { + if !e.dirty || (e.focused && !item.wasFocused) { + showHintIfError = true + } + if e.dirty && !e.focused { + item.wasFocused = true + } + } + + if item.validationError == nil || showHintIfError { + item.helperOutput.Text = item.HintText + item.helperOutput.Color = th.Color(theme.ColorNamePlaceHolder, v) + } else { + item.helperOutput.Text = item.validationError.Error() + item.helperOutput.Color = th.Color(theme.ColorNameError, v) + } + + if item.helperOutput.Text == "" { + item.helperOutput.Hide() + } else { + item.helperOutput.Show() + } + item.helperOutput.Refresh() +} + +func (f *Form) updateLabels() { + th := f.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + for i, item := range f.Items { + l := f.itemGrid.Objects[i*2].(*fyne.Container).Objects[0].(*canvas.Text) + l.TextSize = th.Size(theme.SizeNameText) + if dis, ok := item.Widget.(fyne.Disableable); ok { + if dis.Disabled() { + l.Color = th.Color(theme.ColorNameDisabled, v) + } else { + l.Color = th.Color(theme.ColorNameForeground, v) + } + } else { + l.Color = th.Color(theme.ColorNameForeground, v) + } + + l.Text = item.Text + if f.isVertical() { + l.Alignment = fyne.TextAlignLeading + } else { + l.Alignment = fyne.TextAlignTrailing + } + l.Refresh() + f.updateHelperText(item) + } +} + +// CreateRenderer is a private method to Fyne which links this widget to its renderer +func (f *Form) CreateRenderer() fyne.WidgetRenderer { + f.ExtendBaseWidget(f) + th := f.Theme() + f.cancelButton = &Button{Icon: th.Icon(theme.IconNameCancel), OnTapped: f.OnCancel} + f.submitButton = &Button{Icon: th.Icon(theme.IconNameConfirm), OnTapped: f.OnSubmit, Importance: HighImportance} + buttons := &fyne.Container{Layout: layout.NewGridLayoutWithRows(1), Objects: []fyne.CanvasObject{f.cancelButton, f.submitButton}} + f.buttonBox = &fyne.Container{Layout: layout.NewBorderLayout(nil, nil, nil, buttons), Objects: []fyne.CanvasObject{buttons}} + f.validationError = errFormItemInitialState // set initial state error to guarantee next error (if triggers) is always different + + f.itemGrid = &fyne.Container{Layout: layout.NewFormLayout()} + if f.isVertical() { + f.itemGrid.Layout = layout.NewVBoxLayout() + } else { + f.itemGrid.Layout = layout.NewFormLayout() + } + content := &fyne.Container{Layout: layout.NewVBoxLayout(), Objects: []fyne.CanvasObject{f.itemGrid, f.buttonBox}} + renderer := NewSimpleRenderer(content) + f.ensureRenderItems() + f.updateButtons() + f.updateLabels() + f.checkValidation(nil) // will trigger a validation check for correct initial validation status + return renderer +} + +// NewForm creates a new form widget with the specified rows of form items +// and (if any of them should be shown) a form controls row at the bottom +func NewForm(items ...*FormItem) *Form { + form := &Form{Items: items} + form.ExtendBaseWidget(form) + + return form +} + +type formLabelLayout struct { + form *Form +} + +func (f formLabelLayout) Layout(objs []fyne.CanvasObject, size fyne.Size) { + innerPad := f.form.Theme().Size(theme.SizeNameInnerPadding) + xPad := innerPad + yPos := float32(0) + if !f.form.isVertical() { + xPad += innerPad + yPos = innerPad + } + objs[0].Move(fyne.NewPos(innerPad, yPos)) + objs[0].Resize(fyne.NewSize(size.Width-xPad, objs[0].MinSize().Height)) +} + +func (f formLabelLayout) MinSize(objs []fyne.CanvasObject) fyne.Size { + innerPad := f.form.Theme().Size(theme.SizeNameInnerPadding) + min0 := objs[0].MinSize() + + if !f.form.isVertical() { + min0 = min0.AddWidthHeight(innerPad, 0) + } + + return min0.AddWidthHeight(innerPad, 0) +} + +type formItemLayout struct { + form *Form +} + +func (f formItemLayout) Layout(objs []fyne.CanvasObject, size fyne.Size) { + innerPad := f.form.Theme().Size(theme.SizeNameInnerPadding) + itemHeight := objs[0].MinSize().Height + objs[0].Resize(fyne.NewSize(size.Width, itemHeight)) + + objs[1].Move(fyne.NewPos(innerPad, itemHeight+innerPad/2)) + objs[1].Resize(fyne.NewSize(size.Width, objs[1].MinSize().Width)) +} + +func (f formItemLayout) MinSize(objs []fyne.CanvasObject) fyne.Size { + innerPad := f.form.Theme().Size(theme.SizeNameInnerPadding) + min0 := objs[0].MinSize() + min1 := objs[1].MinSize() + + minWidth := fyne.Max(min0.Width, min1.Width) + height := min0.Height + + items := objs[1].(*fyne.Container).Objects + if len(items) > 0 && items[0].(*canvas.Text).Text != "" { + height += min1.Height + innerPad + } + return fyne.NewSize(minWidth, height) +} diff --git a/vendor/fyne.io/fyne/v2/widget/gridwrap.go b/vendor/fyne.io/fyne/v2/widget/gridwrap.go new file mode 100644 index 0000000..b0a0195 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/gridwrap.go @@ -0,0 +1,705 @@ +package widget + +import ( + "fmt" + "math" + "sort" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/data/binding" + "fyne.io/fyne/v2/driver/desktop" + "fyne.io/fyne/v2/internal/async" + "fyne.io/fyne/v2/internal/widget" + "fyne.io/fyne/v2/theme" +) + +// Declare conformity with interfaces. +var ( + _ fyne.Widget = (*GridWrap)(nil) + _ fyne.Focusable = (*GridWrap)(nil) +) + +// GridWrapItemID is the ID of an individual item in the GridWrap widget. +// +// Since: 2.4 +type GridWrapItemID = int + +// GridWrap is a widget with an API very similar to widget.List, +// that lays out items in a scrollable wrapping grid similar to container.NewGridWrap. +// It caches and reuses widgets for performance. +// +// Since: 2.4 +type GridWrap struct { + BaseWidget + + // Length is a callback for returning the number of items in the GridWrap. + Length func() int `json:"-"` + + // CreateItem is a callback invoked to create a new widget to render + // an item in the GridWrap. + CreateItem func() fyne.CanvasObject `json:"-"` + + // UpdateItem is a callback invoked to update a GridWrap item widget + // to display a new item in the list. The UpdateItem callback should + // only update the given item, it should not invoke APIs that would + // change other properties of the GridWrap itself. + UpdateItem func(id GridWrapItemID, item fyne.CanvasObject) `json:"-"` + + // OnSelected is a callback to be notified when a given item + // in the GridWrap has been selected. + OnSelected func(id GridWrapItemID) `json:"-"` + + // OnSelected is a callback to be notified when a given item + // in the GridWrap has been unselected. + OnUnselected func(id GridWrapItemID) `json:"-"` + + currentFocus ListItemID + focused bool + scroller *widget.Scroll + selected []GridWrapItemID + itemMin fyne.Size + offsetY float32 + offsetUpdated func(fyne.Position) + colCountCache int +} + +// NewGridWrap creates and returns a GridWrap widget for displaying items in +// a wrapping grid layout with scrolling and caching for performance. +// +// Since: 2.4 +func NewGridWrap(length func() int, createItem func() fyne.CanvasObject, updateItem func(GridWrapItemID, fyne.CanvasObject)) *GridWrap { + gwList := &GridWrap{Length: length, CreateItem: createItem, UpdateItem: updateItem} + gwList.ExtendBaseWidget(gwList) + return gwList +} + +// NewGridWrapWithData creates a new GridWrap widget that will display the contents of the provided data. +// +// Since: 2.4 +func NewGridWrapWithData(data binding.DataList, createItem func() fyne.CanvasObject, updateItem func(binding.DataItem, fyne.CanvasObject)) *GridWrap { + gwList := NewGridWrap( + data.Length, + createItem, + func(i GridWrapItemID, o fyne.CanvasObject) { + item, err := data.GetItem(i) + if err != nil { + fyne.LogError(fmt.Sprintf("Error getting data item %d", i), err) + return + } + updateItem(item, o) + }) + + data.AddListener(binding.NewDataListener(gwList.Refresh)) + return gwList +} + +// CreateRenderer is a private method to Fyne which links this widget to its renderer. +func (l *GridWrap) CreateRenderer() fyne.WidgetRenderer { + l.ExtendBaseWidget(l) + + if f := l.CreateItem; f != nil && l.itemMin.IsZero() { + item := createItemAndApplyThemeScope(f, l) + + l.itemMin = item.MinSize() + } + + layout := &fyne.Container{Layout: newGridWrapLayout(l)} + l.scroller = widget.NewVScroll(layout) + layout.Resize(layout.MinSize()) + + return newGridWrapRenderer([]fyne.CanvasObject{l.scroller}, l, l.scroller, layout) +} + +// FocusGained is called after this GridWrap has gained focus. +func (l *GridWrap) FocusGained() { + l.focused = true + l.RefreshItem(l.currentFocus) +} + +// FocusLost is called after this GridWrap has lost focus. +func (l *GridWrap) FocusLost() { + l.focused = false + l.RefreshItem(l.currentFocus) +} + +// MinSize returns the size that this widget should not shrink below. +func (l *GridWrap) MinSize() fyne.Size { + l.ExtendBaseWidget(l) + return l.BaseWidget.MinSize() +} + +func (l *GridWrap) scrollTo(id GridWrapItemID) { + if l.scroller == nil { + return + } + + pad := l.Theme().Size(theme.SizeNamePadding) + row := math.Floor(float64(id) / float64(l.ColumnCount())) + y := float32(row)*l.itemMin.Height + float32(row)*pad + if y < l.scroller.Offset.Y { + l.scroller.Offset.Y = y + } else if size := l.scroller.Size(); y+l.itemMin.Height > l.scroller.Offset.Y+size.Height { + l.scroller.Offset.Y = y + l.itemMin.Height - size.Height + } + l.offsetUpdated(l.scroller.Offset) +} + +// RefreshItem refreshes a single item, specified by the item ID passed in. +// +// Since: 2.4 +func (l *GridWrap) RefreshItem(id GridWrapItemID) { + if l.scroller == nil { + return + } + l.BaseWidget.Refresh() + lo := l.scroller.Content.(*fyne.Container).Layout.(*gridWrapLayout) + item, ok := lo.searchVisible(lo.visible, id) + if ok { + lo.setupGridItem(item, id, l.focused && l.currentFocus == id) + } +} + +// Resize is called when this GridWrap should change size. We refresh to ensure invisible items are drawn. +func (l *GridWrap) Resize(s fyne.Size) { + oldColCount := l.ColumnCount() + oldHeight := l.size.Height + l.colCountCache = 0 + l.BaseWidget.Resize(s) + newColCount := l.ColumnCount() + + if oldColCount == newColCount && oldHeight == s.Height { + // no content update needed if resizing only horizontally and col count is unchanged + return + } + + if l.scroller != nil { + l.offsetUpdated(l.scroller.Offset) + l.scroller.Content.(*fyne.Container).Layout.(*gridWrapLayout).updateGrid(true) + } +} + +// Select adds the item identified by the given ID to the selection. +func (l *GridWrap) Select(id GridWrapItemID) { + if len(l.selected) > 0 && id == l.selected[0] { + return + } + length := 0 + if f := l.Length; f != nil { + length = f() + } + if id < 0 || id >= length { + return + } + old := l.selected + l.selected = []GridWrapItemID{id} + defer func() { + if f := l.OnUnselected; f != nil && len(old) > 0 { + f(old[0]) + } + if f := l.OnSelected; f != nil { + f(id) + } + }() + l.scrollTo(id) + l.Refresh() +} + +// ScrollTo scrolls to the item represented by id +func (l *GridWrap) ScrollTo(id GridWrapItemID) { + length := 0 + if f := l.Length; f != nil { + length = f() + } + if id < 0 || id >= length { + return + } + l.scrollTo(id) + l.Refresh() +} + +// ScrollToBottom scrolls to the end of the list +func (l *GridWrap) ScrollToBottom() { + l.scroller.ScrollToBottom() + l.offsetUpdated(l.scroller.Offset) +} + +// ScrollToTop scrolls to the start of the list +func (l *GridWrap) ScrollToTop() { + l.scroller.ScrollToTop() + l.offsetUpdated(l.scroller.Offset) +} + +// ScrollToOffset scrolls the list to the given offset position +func (l *GridWrap) ScrollToOffset(offset float32) { + if l.scroller == nil { + return + } + if offset < 0 { + offset = 0 + } + contentHeight := l.contentMinSize().Height + if l.Size().Height >= contentHeight { + return // content fully visible - no need to scroll + } + if offset > contentHeight { + offset = contentHeight + } + l.scroller.ScrollToOffset(fyne.NewPos(0, offset)) + l.offsetUpdated(l.scroller.Offset) +} + +// TypedKey is called if a key event happens while this GridWrap is focused. +func (l *GridWrap) TypedKey(event *fyne.KeyEvent) { + switch event.Name { + case fyne.KeySpace: + l.Select(l.currentFocus) + case fyne.KeyDown: + count := 0 + if f := l.Length; f != nil { + count = f() + } + l.RefreshItem(l.currentFocus) + l.currentFocus += l.ColumnCount() + if l.currentFocus >= count-1 { + l.currentFocus = count - 1 + } + l.scrollTo(l.currentFocus) + l.RefreshItem(l.currentFocus) + case fyne.KeyLeft: + if l.currentFocus <= 0 { + return + } + + l.RefreshItem(l.currentFocus) + l.currentFocus-- + l.scrollTo(l.currentFocus) + l.RefreshItem(l.currentFocus) + case fyne.KeyRight: + if f := l.Length; f != nil && l.currentFocus >= f()-1 { + return + } + + l.RefreshItem(l.currentFocus) + l.currentFocus++ + l.scrollTo(l.currentFocus) + l.RefreshItem(l.currentFocus) + case fyne.KeyUp: + if l.currentFocus <= 0 { + return + } + l.RefreshItem(l.currentFocus) + l.currentFocus -= l.ColumnCount() + if l.currentFocus < 0 { + l.currentFocus = 0 + } + l.scrollTo(l.currentFocus) + l.RefreshItem(l.currentFocus) + } +} + +// TypedRune is called if a text event happens while this GridWrap is focused. +func (l *GridWrap) TypedRune(_ rune) { + // intentionally left blank +} + +// GetScrollOffset returns the current scroll offset position +func (l *GridWrap) GetScrollOffset() float32 { + return l.offsetY +} + +// Unselect removes the item identified by the given ID from the selection. +func (l *GridWrap) Unselect(id GridWrapItemID) { + if len(l.selected) == 0 || l.selected[0] != id { + return + } + + l.selected = nil + l.Refresh() + if f := l.OnUnselected; f != nil { + f(id) + } +} + +// UnselectAll removes all items from the selection. +func (l *GridWrap) UnselectAll() { + if len(l.selected) == 0 { + return + } + + selected := l.selected + l.selected = nil + l.Refresh() + if f := l.OnUnselected; f != nil { + for _, id := range selected { + f(id) + } + } +} + +func (l *GridWrap) contentMinSize() fyne.Size { + padding := l.Theme().Size(theme.SizeNamePadding) + if l.Length == nil { + return fyne.NewSize(0, 0) + } + + cols := l.ColumnCount() + rows := float32(math.Ceil(float64(l.Length()) / float64(cols))) + return fyne.NewSize(l.itemMin.Width, (l.itemMin.Height+padding)*rows-padding) +} + +// Declare conformity with WidgetRenderer interface. +var _ fyne.WidgetRenderer = (*gridWrapRenderer)(nil) + +type gridWrapRenderer struct { + objects []fyne.CanvasObject + + list *GridWrap + scroller *widget.Scroll + layout *fyne.Container +} + +func newGridWrapRenderer(objects []fyne.CanvasObject, l *GridWrap, scroller *widget.Scroll, layout *fyne.Container) *gridWrapRenderer { + lr := &gridWrapRenderer{objects: objects, list: l, scroller: scroller, layout: layout} + lr.scroller.OnScrolled = l.offsetUpdated + return lr +} + +func (l *gridWrapRenderer) Layout(size fyne.Size) { + l.scroller.Resize(size) +} + +func (l *gridWrapRenderer) MinSize() fyne.Size { + return l.scroller.MinSize().Max(l.list.itemMin) +} + +func (l *gridWrapRenderer) Refresh() { + if f := l.list.CreateItem; f != nil { + item := createItemAndApplyThemeScope(f, l.list) + + l.list.itemMin = item.MinSize() + } + l.Layout(l.list.Size()) + l.scroller.Refresh() + l.layout.Layout.(*gridWrapLayout).updateGrid(false) + canvas.Refresh(l.list) +} + +func (l *gridWrapRenderer) Destroy() { +} + +func (l *gridWrapRenderer) Objects() []fyne.CanvasObject { + return l.objects +} + +// Declare conformity with interfaces. +var ( + _ fyne.Widget = (*gridWrapItem)(nil) + _ fyne.Tappable = (*gridWrapItem)(nil) + _ desktop.Hoverable = (*gridWrapItem)(nil) +) + +type gridWrapItem struct { + BaseWidget + + onTapped func() + background *canvas.Rectangle + child fyne.CanvasObject + hovered, selected bool +} + +func newGridWrapItem(child fyne.CanvasObject, tapped func()) *gridWrapItem { + gw := &gridWrapItem{ + child: child, + onTapped: tapped, + } + + gw.ExtendBaseWidget(gw) + return gw +} + +// CreateRenderer is a private method to Fyne which links this widget to its renderer. +func (gw *gridWrapItem) CreateRenderer() fyne.WidgetRenderer { + gw.ExtendBaseWidget(gw) + th := gw.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + gw.background = canvas.NewRectangle(th.Color(theme.ColorNameHover, v)) + gw.background.CornerRadius = th.Size(theme.SizeNameSelectionRadius) + gw.background.Hide() + + objects := []fyne.CanvasObject{gw.background, gw.child} + + return &gridWrapItemRenderer{widget.NewBaseRenderer(objects), gw} +} + +// MinSize returns the size that this widget should not shrink below. +func (gw *gridWrapItem) MinSize() fyne.Size { + gw.ExtendBaseWidget(gw) + return gw.BaseWidget.MinSize() +} + +// MouseIn is called when a desktop pointer enters the widget. +func (gw *gridWrapItem) MouseIn(*desktop.MouseEvent) { + gw.hovered = true + gw.Refresh() +} + +// MouseMoved is called when a desktop pointer hovers over the widget. +func (gw *gridWrapItem) MouseMoved(*desktop.MouseEvent) { +} + +// MouseOut is called when a desktop pointer exits the widget. +func (gw *gridWrapItem) MouseOut() { + gw.hovered = false + gw.Refresh() +} + +// Tapped is called when a pointer tapped event is captured and triggers any tap handler. +func (gw *gridWrapItem) Tapped(*fyne.PointEvent) { + if gw.onTapped != nil { + gw.selected = true + gw.Refresh() + gw.onTapped() + } +} + +// Declare conformity with the WidgetRenderer interface. +var _ fyne.WidgetRenderer = (*gridWrapItemRenderer)(nil) + +type gridWrapItemRenderer struct { + widget.BaseRenderer + + item *gridWrapItem +} + +// MinSize calculates the minimum size of a listItem. +// This is based on the size of the status indicator and the size of the child object. +func (gw *gridWrapItemRenderer) MinSize() fyne.Size { + return gw.item.child.MinSize() +} + +// Layout the components of the listItem widget. +func (gw *gridWrapItemRenderer) Layout(size fyne.Size) { + gw.item.background.Resize(size) + gw.item.child.Resize(size) +} + +func (gw *gridWrapItemRenderer) Refresh() { + th := gw.item.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + gw.item.background.CornerRadius = th.Size(theme.SizeNameSelectionRadius) + if gw.item.selected { + gw.item.background.FillColor = th.Color(theme.ColorNameSelection, v) + gw.item.background.Show() + } else if gw.item.hovered { + gw.item.background.FillColor = th.Color(theme.ColorNameHover, v) + gw.item.background.Show() + } else { + gw.item.background.Hide() + } + gw.item.background.Refresh() + canvas.Refresh(gw.item.super()) +} + +// Declare conformity with Layout interface. +var _ fyne.Layout = (*gridWrapLayout)(nil) + +type gridItemAndID struct { + item *gridWrapItem + id GridWrapItemID +} + +type gridWrapLayout struct { + gw *GridWrap + + itemPool async.Pool[fyne.CanvasObject] + visible []gridItemAndID + wasVisible []gridItemAndID +} + +func newGridWrapLayout(gw *GridWrap) fyne.Layout { + gwl := &gridWrapLayout{gw: gw} + gw.offsetUpdated = gwl.offsetUpdated + return gwl +} + +func (l *gridWrapLayout) Layout(_ []fyne.CanvasObject, _ fyne.Size) { + l.updateGrid(true) +} + +func (l *gridWrapLayout) MinSize(_ []fyne.CanvasObject) fyne.Size { + return l.gw.contentMinSize() +} + +func (l *gridWrapLayout) getItem() *gridWrapItem { + item := l.itemPool.Get() + if item == nil { + if f := l.gw.CreateItem; f != nil { + child := createItemAndApplyThemeScope(f, l.gw) + + item = newGridWrapItem(child, nil) + } + } + return item.(*gridWrapItem) +} + +func (l *gridWrapLayout) offsetUpdated(pos fyne.Position) { + if l.gw.offsetY == pos.Y { + return + } + l.gw.offsetY = pos.Y + l.updateGrid(true) +} + +func (l *gridWrapLayout) setupGridItem(li *gridWrapItem, id GridWrapItemID, focus bool) { + previousIndicator := li.selected + li.selected = false + for _, s := range l.gw.selected { + if id == s { + li.selected = true + break + } + } + if focus { + li.hovered = true + li.Refresh() + } else if previousIndicator != li.selected || li.hovered { + li.hovered = false + li.Refresh() + } + if f := l.gw.UpdateItem; f != nil { + f(id, li.child) + } + li.onTapped = func() { + if !fyne.CurrentDevice().IsMobile() { + l.gw.RefreshItem(l.gw.currentFocus) + canvas := fyne.CurrentApp().Driver().CanvasForObject(l.gw) + if canvas != nil { + canvas.Focus(l.gw.impl.(fyne.Focusable)) + } + + l.gw.currentFocus = id + } + + l.gw.Select(id) + } +} + +// ColumnCount returns the number of columns that are/will be shown +// in this GridWrap, based on the widget's current width. +// +// Since: 2.5 +func (l *GridWrap) ColumnCount() int { + if l.colCountCache < 1 { + padding := l.Theme().Size(theme.SizeNamePadding) + l.colCountCache = 1 + width := l.Size().Width + if width > l.itemMin.Width { + l.colCountCache = int(math.Floor(float64(width+padding) / float64(l.itemMin.Width+padding))) + } + } + return l.colCountCache +} + +func (l *gridWrapLayout) updateGrid(newOnly bool) { + // code here is a mashup of listLayout.updateList and gridWrapLayout.Layout + padding := l.gw.Theme().Size(theme.SizeNamePadding) + + length := 0 + if f := l.gw.Length; f != nil { + length = f() + } + + colCount := l.gw.ColumnCount() + visibleRowsCount := int(math.Ceil(float64(l.gw.scroller.Size().Height)/float64(l.gw.itemMin.Height+padding))) + 1 + + offY := l.gw.offsetY - float32(math.Mod(float64(l.gw.offsetY), float64(l.gw.itemMin.Height+padding))) + minRow := int(offY / (l.gw.itemMin.Height + padding)) + minItem := minRow * colCount + maxRow := int(math.Min(float64(minRow+visibleRowsCount), math.Ceil(float64(length)/float64(colCount)))) + maxItem := GridWrapItemID(math.Min(float64(maxRow*colCount), float64(length-1))) + + if l.gw.UpdateItem == nil { + fyne.LogError("Missing UpdateCell callback required for GridWrap", nil) + } + + // l.wasVisible now represents the currently visible items, while + // l.visible will be updated to represent what is visible *after* the update + l.wasVisible = append(l.wasVisible, l.visible...) + l.visible = l.visible[:0] + + c := l.gw.scroller.Content.(*fyne.Container) + oldObjLen := len(c.Objects) + c.Objects = c.Objects[:0] + y := offY + curItemID := minItem + for row := minRow; row <= maxRow && curItemID <= maxItem; row++ { + x := float32(0) + for col := 0; col < colCount && curItemID <= maxItem; col++ { + item, ok := l.searchVisible(l.wasVisible, curItemID) + if !ok { + item = l.getItem() + if item == nil { + continue + } + item.Resize(l.gw.itemMin) + } + + item.Move(fyne.NewPos(x, y)) + item.Resize(l.gw.itemMin) + + x += l.gw.itemMin.Width + padding + l.visible = append(l.visible, gridItemAndID{item: item, id: curItemID}) + c.Objects = append(c.Objects, item) + curItemID++ + } + y += l.gw.itemMin.Height + padding + } + l.nilOldSliceData(c.Objects, len(c.Objects), oldObjLen) + + for _, old := range l.wasVisible { + if _, ok := l.searchVisible(l.visible, old.id); !ok { + l.itemPool.Put(old.item) + } + } + + if newOnly { + for _, obj := range l.visible { + if _, ok := l.searchVisible(l.wasVisible, obj.id); !ok { + l.setupGridItem(obj.item, obj.id, l.gw.focused && l.gw.currentFocus == obj.id) + } + } + } else { + for _, obj := range l.visible { + l.setupGridItem(obj.item, obj.id, l.gw.focused && l.gw.currentFocus == obj.id) + } + } + + // we don't need wasVisible now until next call to update + // nil out all references before truncating slice + for i := 0; i < len(l.wasVisible); i++ { + l.wasVisible[i].item = nil + } + l.wasVisible = l.wasVisible[:0] +} + +// invariant: visible is in ascending order of IDs +func (l *gridWrapLayout) searchVisible(visible []gridItemAndID, id GridWrapItemID) (*gridWrapItem, bool) { + ln := len(visible) + idx := sort.Search(ln, func(i int) bool { return visible[i].id >= id }) + if idx < ln && visible[idx].id == id { + return visible[idx].item, true + } + return nil, false +} + +func (l *gridWrapLayout) nilOldSliceData(objs []fyne.CanvasObject, len, oldLen int) { + if oldLen > len { + objs = objs[:oldLen] // gain view into old data + for i := len; i < oldLen; i++ { + objs[i] = nil + } + } +} diff --git a/vendor/fyne.io/fyne/v2/widget/hyperlink.go b/vendor/fyne.io/fyne/v2/widget/hyperlink.go new file mode 100644 index 0000000..c1297da --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/hyperlink.go @@ -0,0 +1,336 @@ +package widget + +import ( + "image/color" + "net/url" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/driver/desktop" + "fyne.io/fyne/v2/internal/widget" + "fyne.io/fyne/v2/theme" +) + +var ( + _ fyne.Focusable = (*Hyperlink)(nil) + _ fyne.Widget = (*Hyperlink)(nil) +) + +// Hyperlink widget is a text component with appropriate padding and layout. +// When clicked, the default web browser should open with a URL +type Hyperlink struct { + BaseWidget + Text string + URL *url.URL + Alignment fyne.TextAlign // The alignment of the Text + Wrapping fyne.TextWrap // The wrapping of the Text + TextStyle fyne.TextStyle // The style of the hyperlink text + + // The truncation mode of the hyperlink + // + // Since: 2.5 + Truncation fyne.TextTruncation + + // The theme size name for the text size of the hyperlink + // + // Since: 2.5 + SizeName fyne.ThemeSizeName + + // OnTapped overrides the default `fyne.OpenURL` call when the link is tapped + // + // Since: 2.2 + OnTapped func() `json:"-"` + + textSize fyne.Size // updated in syncSegments + focused, hovered bool + provider RichText +} + +// NewHyperlink creates a new hyperlink widget with the set text content +func NewHyperlink(text string, url *url.URL) *Hyperlink { + return NewHyperlinkWithStyle(text, url, fyne.TextAlignLeading, fyne.TextStyle{}) +} + +// NewHyperlinkWithStyle creates a new hyperlink widget with the set text content +func NewHyperlinkWithStyle(text string, url *url.URL, alignment fyne.TextAlign, style fyne.TextStyle) *Hyperlink { + hl := &Hyperlink{ + Text: text, + URL: url, + Alignment: alignment, + TextStyle: style, + } + + return hl +} + +// CreateRenderer is a private method to Fyne which links this widget to its renderer +func (hl *Hyperlink) CreateRenderer() fyne.WidgetRenderer { + hl.ExtendBaseWidget(hl) + hl.provider.ExtendBaseWidget(&hl.provider) + hl.syncSegments() + + th := hl.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + focus := canvas.NewRectangle(color.Transparent) + focus.StrokeColor = th.Color(theme.ColorNameFocus, v) + focus.StrokeWidth = 2 + focus.Hide() + under := canvas.NewRectangle(th.Color(theme.ColorNameHyperlink, v)) + under.Hide() + return &hyperlinkRenderer{hl: hl, objects: []fyne.CanvasObject{&hl.provider, focus, under}, focus: focus, under: under} +} + +// Cursor returns the cursor type of this widget +func (hl *Hyperlink) Cursor() desktop.Cursor { + if hl.hovered { + return desktop.PointerCursor + } + return desktop.DefaultCursor +} + +// FocusGained is a hook called by the focus handling logic after this object gained the focus. +func (hl *Hyperlink) FocusGained() { + hl.focused = true + hl.BaseWidget.Refresh() +} + +// FocusLost is a hook called by the focus handling logic after this object lost the focus. +func (hl *Hyperlink) FocusLost() { + hl.focused = false + hl.BaseWidget.Refresh() +} + +// MouseIn is a hook that is called if the mouse pointer enters the element. +func (hl *Hyperlink) MouseIn(e *desktop.MouseEvent) { + hl.MouseMoved(e) +} + +// MouseMoved is a hook that is called if the mouse pointer moved over the element. +func (hl *Hyperlink) MouseMoved(e *desktop.MouseEvent) { + oldHovered := hl.hovered + hl.hovered = hl.isPosOverText(e.Position) + if hl.hovered != oldHovered { + hl.BaseWidget.Refresh() + } +} + +// MouseOut is a hook that is called if the mouse pointer leaves the element. +func (hl *Hyperlink) MouseOut() { + changed := hl.hovered + hl.hovered = false + if changed { + hl.BaseWidget.Refresh() + } +} + +func (hl *Hyperlink) focusWidth() float32 { + th := hl.Theme() + + innerPad := th.Size(theme.SizeNameInnerPadding) + return fyne.Min(hl.Size().Width, hl.textSize.Width+innerPad+th.Size(theme.SizeNamePadding)*2) - innerPad +} + +func (hl *Hyperlink) focusXPos() float32 { + innerPad := hl.Theme().Size(theme.SizeNameInnerPadding) + + switch hl.Alignment { + case fyne.TextAlignLeading: + return innerPad / 2 + case fyne.TextAlignCenter: + return (hl.Size().Width - hl.focusWidth()) / 2 + case fyne.TextAlignTrailing: + return (hl.Size().Width - hl.focusWidth()) - innerPad/2 + default: + return 0 // unreached + } +} + +func (hl *Hyperlink) isPosOverText(pos fyne.Position) bool { + th := hl.Theme() + innerPad := th.Size(theme.SizeNameInnerPadding) + pad := th.Size(theme.SizeNamePadding) + lineCount := fyne.Max(1, float32(len(hl.provider.rowBounds))) + + xpos := hl.focusXPos() + return pos.X >= xpos && pos.X <= xpos+hl.focusWidth() && + pos.Y >= innerPad/2 && pos.Y <= hl.textSize.Height*lineCount+pad*2+innerPad/2 +} + +// Refresh triggers a redraw of the hyperlink. +func (hl *Hyperlink) Refresh() { + if len(hl.provider.Segments) == 0 { + return // Not initialized yet. + } + + hl.syncSegments() + hl.provider.Refresh() + hl.BaseWidget.Refresh() +} + +// MinSize returns the smallest size this widget can shrink to +func (hl *Hyperlink) MinSize() fyne.Size { + hl.ExtendBaseWidget(hl) + return hl.BaseWidget.MinSize() +} + +// Resize sets a new size for the hyperlink. +// Note this should not be used if the widget is being managed by a Layout within a Container. +func (hl *Hyperlink) Resize(size fyne.Size) { + hl.BaseWidget.Resize(size) + + if len(hl.provider.Segments) == 0 { + return // Not initialized yet. + } + hl.provider.Resize(size) +} + +// SetText sets the text of the hyperlink +func (hl *Hyperlink) SetText(text string) { + hl.Text = text + + if len(hl.provider.Segments) == 0 { + return // Not initialized yet. + } + hl.syncSegments() + hl.provider.Refresh() +} + +// SetURL sets the URL of the hyperlink, taking in a URL type +func (hl *Hyperlink) SetURL(url *url.URL) { + hl.URL = url +} + +// SetURLFromString sets the URL of the hyperlink, taking in a string type +func (hl *Hyperlink) SetURLFromString(str string) error { + u, err := url.Parse(str) + if err != nil { + return err + } + hl.SetURL(u) + return nil +} + +// Tapped is called when a pointer tapped event is captured and triggers any change handler +func (hl *Hyperlink) Tapped(e *fyne.PointEvent) { + if len(hl.provider.Segments) != 0 && !hl.isPosOverText(e.Position) { + return // tapped outside text area + } + hl.invokeAction() +} + +func (hl *Hyperlink) invokeAction() { + onTapped := hl.OnTapped + + if onTapped != nil { + onTapped() + return + } + hl.openURL() +} + +// TypedRune is a hook called by the input handling logic on text input events if this object is focused. +func (hl *Hyperlink) TypedRune(rune) { +} + +// TypedKey is a hook called by the input handling logic on key events if this object is focused. +func (hl *Hyperlink) TypedKey(ev *fyne.KeyEvent) { + if ev.Name == fyne.KeySpace { + hl.invokeAction() + } +} + +func (hl *Hyperlink) openURL() { + url := hl.URL + + if url != nil { + err := fyne.CurrentApp().OpenURL(url) + if err != nil { + fyne.LogError("Failed to open url", err) + } + } +} + +func (hl *Hyperlink) syncSegments() { + th := hl.Theme() + + hl.provider.Wrapping = hl.Wrapping + hl.provider.Truncation = hl.Truncation + + if len(hl.provider.Segments) == 0 { + hl.provider.Scroll = widget.ScrollNone + hl.provider.Segments = []RichTextSegment{ + &TextSegment{ + Style: RichTextStyle{ + Alignment: hl.Alignment, + ColorName: theme.ColorNameHyperlink, + Inline: true, + TextStyle: hl.TextStyle, + }, + Text: hl.Text, + }, + } + } else { + segment := hl.provider.Segments[0].(*TextSegment) + segment.Style.Alignment = hl.Alignment + segment.Style.TextStyle = hl.TextStyle + segment.Text = hl.Text + } + + sizeName := hl.SizeName + if sizeName == "" { + sizeName = theme.SizeNameText + } + hl.provider.Segments[0].(*TextSegment).Style.SizeName = sizeName + hl.textSize = fyne.MeasureText(hl.Text, th.Size(sizeName), hl.TextStyle) +} + +var _ fyne.WidgetRenderer = (*hyperlinkRenderer)(nil) + +type hyperlinkRenderer struct { + hl *Hyperlink + focus *canvas.Rectangle + under *canvas.Rectangle + + objects []fyne.CanvasObject +} + +func (r *hyperlinkRenderer) Destroy() { +} + +func (r *hyperlinkRenderer) Layout(s fyne.Size) { + th := r.hl.Theme() + textSize := r.hl.textSize + innerPad := th.Size(theme.SizeNameInnerPadding) + w := r.hl.focusWidth() + xposFocus := r.hl.focusXPos() + + xposUnderline := xposFocus + innerPad/2 + lineCount := float32(len(r.hl.provider.rowBounds)) + + r.hl.provider.Resize(s) + r.focus.Move(fyne.NewPos(xposFocus, innerPad/2)) + r.focus.Resize(fyne.NewSize(w, textSize.Height*lineCount+innerPad)) + r.under.Move(fyne.NewPos(xposUnderline, textSize.Height*lineCount+th.Size(theme.SizeNamePadding)*2)) + r.under.Resize(fyne.NewSize(w-innerPad, 1)) +} + +func (r *hyperlinkRenderer) MinSize() fyne.Size { + return r.hl.provider.MinSize() +} + +func (r *hyperlinkRenderer) Objects() []fyne.CanvasObject { + return r.objects +} + +func (r *hyperlinkRenderer) Refresh() { + r.hl.provider.Refresh() + th := r.hl.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + r.focus.StrokeColor = th.Color(theme.ColorNameFocus, v) + r.focus.Hidden = !r.hl.focused + r.focus.Refresh() + r.under.FillColor = th.Color(theme.ColorNameHyperlink, v) + r.under.Hidden = !r.hl.hovered + r.under.Refresh() +} diff --git a/vendor/fyne.io/fyne/v2/widget/icon.go b/vendor/fyne.io/fyne/v2/widget/icon.go new file mode 100644 index 0000000..7ab35f1 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/icon.go @@ -0,0 +1,84 @@ +package widget + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/internal/widget" + "fyne.io/fyne/v2/theme" +) + +type iconRenderer struct { + widget.BaseRenderer + raster *canvas.Image + + image *Icon +} + +func (i *iconRenderer) MinSize() fyne.Size { + return fyne.NewSquareSize(i.image.Theme().Size(theme.SizeNameInlineIcon)) +} + +func (i *iconRenderer) Layout(size fyne.Size) { + if len(i.Objects()) == 0 { + return + } + + i.Objects()[0].Resize(size) +} + +func (i *iconRenderer) Refresh() { + if i.image.Resource == i.image.cachedRes { + return + } + + i.raster.Resource = i.image.Resource + i.image.cachedRes = i.image.Resource + + if i.image.Resource == nil { + i.raster.Image = nil // reset the internal caching too... + } + + i.raster.Refresh() +} + +// Icon widget is a basic image component that load's its resource to match the theme. +type Icon struct { + BaseWidget + + Resource fyne.Resource // The resource for this icon + cachedRes fyne.Resource +} + +// SetResource updates the resource rendered in this icon widget +func (i *Icon) SetResource(res fyne.Resource) { + i.Resource = res + i.Refresh() +} + +// MinSize returns the size that this widget should not shrink below +func (i *Icon) MinSize() fyne.Size { + i.ExtendBaseWidget(i) + return i.BaseWidget.MinSize() +} + +// CreateRenderer is a private method to Fyne which links this widget to its renderer +func (i *Icon) CreateRenderer() fyne.WidgetRenderer { + i.ExtendBaseWidget(i) + + img := canvas.NewImageFromResource(i.Resource) + img.FillMode = canvas.ImageFillContain + + r := &iconRenderer{image: i, raster: img} + r.SetObjects([]fyne.CanvasObject{img}) + i.cachedRes = i.Resource + return r +} + +// NewIcon returns a new icon widget that displays a themed icon resource +func NewIcon(res fyne.Resource) *Icon { + icon := &Icon{} + icon.ExtendBaseWidget(icon) + icon.SetResource(res) // force the image conversion + + return icon +} diff --git a/vendor/fyne.io/fyne/v2/widget/importance.go b/vendor/fyne.io/fyne/v2/widget/importance.go new file mode 100644 index 0000000..0a395b0 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/importance.go @@ -0,0 +1,29 @@ +package widget + +// Importance represents how prominent the widget should appear +// +// Since: 2.4 +type Importance int + +const ( + // MediumImportance applies a standard appearance. + MediumImportance Importance = iota + // HighImportance applies a prominent appearance. + HighImportance + // LowImportance applies a subtle appearance. + LowImportance + + // DangerImportance applies an error theme to the widget. + // + // Since: 2.3 + DangerImportance + // WarningImportance applies a warning theme to the widget. + // + // Since: 2.3 + WarningImportance + + // SuccessImportance applies a success theme to the widget. + // + // Since: 2.4 + SuccessImportance +) diff --git a/vendor/fyne.io/fyne/v2/widget/label.go b/vendor/fyne.io/fyne/v2/widget/label.go new file mode 100644 index 0000000..4fca03e --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/label.go @@ -0,0 +1,246 @@ +package widget + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/data/binding" + "fyne.io/fyne/v2/theme" +) + +var _ fyne.Widget = (*Label)(nil) + +// Label widget is a label component with appropriate padding and layout. +type Label struct { + BaseWidget + Text string + Alignment fyne.TextAlign // The alignment of the text + Wrapping fyne.TextWrap // The wrapping of the text + TextStyle fyne.TextStyle // The style of the label text + + // The truncation mode of the text + // + // Since: 2.4 + Truncation fyne.TextTruncation + // Importance informs how the label should be styled, i.e. warning or disabled + // + // Since: 2.4 + Importance Importance + + // The theme size name for the text size of the label + // + // Since: 2.6 + SizeName fyne.ThemeSizeName + + // If set to true, Selectable indicates that this label should support select interaction + // to allow the text to be copied. + // + //Since: 2.6 + Selectable bool + + provider *RichText + binder basicBinder + selection *focusSelectable +} + +// NewLabel creates a new label widget with the set text content +func NewLabel(text string) *Label { + return NewLabelWithStyle(text, fyne.TextAlignLeading, fyne.TextStyle{}) +} + +// NewLabelWithData returns a Label widget connected to the specified data source. +// +// Since: 2.0 +func NewLabelWithData(data binding.String) *Label { + label := NewLabel("") + label.Bind(data) + + return label +} + +// NewLabelWithStyle creates a new label widget with the set text content +func NewLabelWithStyle(text string, alignment fyne.TextAlign, style fyne.TextStyle) *Label { + l := &Label{ + Text: text, + Alignment: alignment, + TextStyle: style, + } + + l.ExtendBaseWidget(l) + return l +} + +// Bind connects the specified data source to this Label. +// The current value will be displayed and any changes in the data will cause the widget to update. +// +// Since: 2.0 +func (l *Label) Bind(data binding.String) { + l.binder.SetCallback(l.updateFromData) // This could only be done once, maybe in ExtendBaseWidget? + l.binder.Bind(data) +} + +// CreateRenderer is a private method to Fyne which links this widget to its renderer +func (l *Label) CreateRenderer() fyne.WidgetRenderer { + l.provider = NewRichTextWithText(l.Text) + l.ExtendBaseWidget(l) + l.syncSegments() + + l.selection = &focusSelectable{} + l.selection.ExtendBaseWidget(l.selection) + l.selection.focus = l.selection + l.selection.style = l.TextStyle + l.selection.theme = l.Theme() + l.selection.provider = l.provider + + return &labelRenderer{l} +} + +// MinSize returns the size that this label should not shrink below. +func (l *Label) MinSize() fyne.Size { + l.ExtendBaseWidget(l) + return l.BaseWidget.MinSize() +} + +// Refresh triggers a redraw of the label. +func (l *Label) Refresh() { + if l.provider == nil { // not created until visible + return + } + l.syncSegments() + l.provider.Refresh() + l.BaseWidget.Refresh() +} + +// SelectedText returns the text currently selected in this Label. +// If the label is not Selectable it will return an empty string. +// If there is no selection it will return the empty string. +// +// Since: 2.6 +func (l *Label) SelectedText() string { + if !l.Selectable || l.selection == nil { + return "" + } + + return l.selection.SelectedText() +} + +// SetText sets the text of the label +func (l *Label) SetText(text string) { + l.Text = text + l.Refresh() +} + +// Unbind disconnects any configured data source from this Label. +// The current value will remain at the last value of the data source. +// +// Since: 2.0 +func (l *Label) Unbind() { + l.binder.Unbind() +} + +func (l *Label) syncSegments() { + var color fyne.ThemeColorName + switch l.Importance { + case LowImportance: + color = theme.ColorNameDisabled + case MediumImportance: + color = theme.ColorNameForeground + case HighImportance: + color = theme.ColorNamePrimary + case DangerImportance: + color = theme.ColorNameError + case WarningImportance: + color = theme.ColorNameWarning + case SuccessImportance: + color = theme.ColorNameSuccess + default: + color = theme.ColorNameForeground + } + + sizeName := l.SizeName + if sizeName == "" { + sizeName = theme.SizeNameText + } + l.provider.Wrapping = l.Wrapping + l.provider.Truncation = l.Truncation + l.provider.Segments[0].(*TextSegment).Style = RichTextStyle{ + Alignment: l.Alignment, + ColorName: color, + Inline: true, + TextStyle: l.TextStyle, + SizeName: sizeName, + } + l.provider.Segments[0].(*TextSegment).Text = l.Text +} + +func (l *Label) updateFromData(data binding.DataItem) { + if data == nil { + return + } + textSource, ok := data.(binding.String) + if !ok { + return + } + val, err := textSource.Get() + if err != nil { + fyne.LogError("Error getting current data value", err) + return + } + l.SetText(val) +} + +type labelRenderer struct { + l *Label +} + +func (r *labelRenderer) Destroy() { +} + +func (r *labelRenderer) Layout(s fyne.Size) { + r.l.selection.Resize(s) + r.l.provider.Resize(s) +} + +func (r *labelRenderer) MinSize() fyne.Size { + return r.l.provider.MinSize() +} + +func (r *labelRenderer) Objects() []fyne.CanvasObject { + if !r.l.Selectable { + return []fyne.CanvasObject{r.l.provider} + } + + return []fyne.CanvasObject{r.l.selection, r.l.provider} +} + +func (r *labelRenderer) Refresh() { + r.l.provider.Refresh() + + sel := r.l.selection + if !r.l.Selectable || sel == nil { + return + } + + sel.sizeName = r.l.SizeName + sel.style = r.l.TextStyle + sel.theme = r.l.Theme() + sel.Refresh() +} + +type focusSelectable struct { + selectable +} + +func (f *focusSelectable) FocusGained() { + f.focussed = true + f.Refresh() +} + +func (f *focusSelectable) FocusLost() { + f.focussed = false + f.Refresh() +} + +func (f *focusSelectable) TypedKey(*fyne.KeyEvent) { +} + +func (f *focusSelectable) TypedRune(rune) { +} diff --git a/vendor/fyne.io/fyne/v2/widget/list.go b/vendor/fyne.io/fyne/v2/widget/list.go new file mode 100644 index 0000000..310f4cd --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/list.go @@ -0,0 +1,836 @@ +package widget + +import ( + "fmt" + "math" + "sort" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/data/binding" + "fyne.io/fyne/v2/driver/desktop" + "fyne.io/fyne/v2/internal/async" + "fyne.io/fyne/v2/internal/cache" + "fyne.io/fyne/v2/internal/widget" + "fyne.io/fyne/v2/theme" +) + +// ListItemID uniquely identifies an item within a list. +type ListItemID = int + +// Declare conformity with interfaces. +var ( + _ fyne.Widget = (*List)(nil) + _ fyne.Focusable = (*List)(nil) +) + +// List is a widget that pools list items for performance and +// lays the items out in a vertical direction inside of a scroller. +// By default, List requires that all items are the same size, but specific +// rows can have their heights set with SetItemHeight. +// +// Since: 1.4 +type List struct { + BaseWidget + + // Length is a callback for returning the number of items in the list. + Length func() int `json:"-"` + + // CreateItem is a callback invoked to create a new widget to render + // a row in the list. + CreateItem func() fyne.CanvasObject `json:"-"` + + // UpdateItem is a callback invoked to update a list row widget + // to display a new row in the list. The UpdateItem callback should + // only update the given item, it should not invoke APIs that would + // change other properties of the list itself. + UpdateItem func(id ListItemID, item fyne.CanvasObject) `json:"-"` + + // OnSelected is a callback to be notified when a given item + // in the list has been selected. + OnSelected func(id ListItemID) `json:"-"` + + // OnSelected is a callback to be notified when a given item + // in the list has been unselected. + OnUnselected func(id ListItemID) `json:"-"` + + // HideSeparators hides the separators between list rows + // + // Since: 2.5 + HideSeparators bool + + currentFocus ListItemID + focused bool + scroller *widget.Scroll + selected []ListItemID + itemMin fyne.Size + itemHeights map[ListItemID]float32 + offsetY float32 + offsetUpdated func(fyne.Position) +} + +// NewList creates and returns a list widget for displaying items in +// a vertical layout with scrolling and caching for performance. +// +// Since: 1.4 +func NewList(length func() int, createItem func() fyne.CanvasObject, updateItem func(ListItemID, fyne.CanvasObject)) *List { + list := &List{Length: length, CreateItem: createItem, UpdateItem: updateItem} + list.ExtendBaseWidget(list) + return list +} + +// NewListWithData creates a new list widget that will display the contents of the provided data. +// +// Since: 2.0 +func NewListWithData(data binding.DataList, createItem func() fyne.CanvasObject, updateItem func(binding.DataItem, fyne.CanvasObject)) *List { + l := NewList( + data.Length, + createItem, + func(i ListItemID, o fyne.CanvasObject) { + item, err := data.GetItem(i) + if err != nil { + fyne.LogError(fmt.Sprintf("Error getting data item %d", i), err) + return + } + updateItem(item, o) + }) + + data.AddListener(binding.NewDataListener(l.Refresh)) + return l +} + +// CreateRenderer is a private method to Fyne which links this widget to its renderer. +func (l *List) CreateRenderer() fyne.WidgetRenderer { + l.ExtendBaseWidget(l) + + if f := l.CreateItem; f != nil && l.itemMin.IsZero() { + item := createItemAndApplyThemeScope(f, l) + + l.itemMin = item.MinSize() + } + + layout := &fyne.Container{Layout: newListLayout(l)} + l.scroller = widget.NewVScroll(layout) + layout.Resize(layout.MinSize()) + objects := []fyne.CanvasObject{l.scroller} + return newListRenderer(objects, l, l.scroller, layout) +} + +// FocusGained is called after this List has gained focus. +func (l *List) FocusGained() { + l.focused = true + l.RefreshItem(l.currentFocus) +} + +// FocusLost is called after this List has lost focus. +func (l *List) FocusLost() { + l.focused = false + l.RefreshItem(l.currentFocus) +} + +// MinSize returns the size that this widget should not shrink below. +func (l *List) MinSize() fyne.Size { + l.ExtendBaseWidget(l) + return l.BaseWidget.MinSize() +} + +// RefreshItem refreshes a single item, specified by the item ID passed in. +// +// Since: 2.4 +func (l *List) RefreshItem(id ListItemID) { + if l.scroller == nil { + return + } + l.BaseWidget.Refresh() + lo := l.scroller.Content.(*fyne.Container).Layout.(*listLayout) + item, ok := lo.searchVisible(lo.visible, id) + if ok { + lo.setupListItem(item, id, l.focused && l.currentFocus == id) + } +} + +// SetItemHeight supports changing the height of the specified list item. Items normally take the height of the template +// returned from the CreateItem callback. The height parameter uses the same units as a fyne.Size type and refers +// to the internal content height not including the divider size. +// +// Since: 2.3 +func (l *List) SetItemHeight(id ListItemID, height float32) { + if l.itemHeights == nil { + l.itemHeights = make(map[ListItemID]float32) + } + + refresh := l.itemHeights[id] != height + l.itemHeights[id] = height + + if refresh { + l.RefreshItem(id) + } +} + +func (l *List) scrollTo(id ListItemID) { + if l.scroller == nil { + return + } + + separatorThickness := l.Theme().Size(theme.SizeNamePadding) + y := float32(0) + lastItemHeight := l.itemMin.Height + + if len(l.itemHeights) == 0 { + y = (float32(id) * l.itemMin.Height) + (float32(id) * separatorThickness) + } else { + i := 0 + for ; i < id; i++ { + height := l.itemMin.Height + if h, ok := l.itemHeights[i]; ok { + height = h + } + + y += height + separatorThickness + } + lastItemHeight = l.itemMin.Height + if h, ok := l.itemHeights[i]; ok { + lastItemHeight = h + } + } + if y < l.scroller.Offset.Y { + l.scroller.Offset.Y = y + } else if y+l.itemMin.Height > l.scroller.Offset.Y+l.scroller.Size().Height { + l.scroller.Offset.Y = y + lastItemHeight - l.scroller.Size().Height + } + l.offsetUpdated(l.scroller.Offset) +} + +// Resize is called when this list should change size. We refresh to ensure invisible items are drawn. +func (l *List) Resize(s fyne.Size) { + l.BaseWidget.Resize(s) + if l.scroller == nil { + return + } + + l.offsetUpdated(l.scroller.Offset) + l.scroller.Content.(*fyne.Container).Layout.(*listLayout).updateList(true) +} + +// Select add the item identified by the given ID to the selection. +func (l *List) Select(id ListItemID) { + if len(l.selected) > 0 && id == l.selected[0] { + return + } + length := 0 + if f := l.Length; f != nil { + length = f() + } + if id < 0 || id >= length { + return + } + old := l.selected + l.selected = []ListItemID{id} + defer func() { + if f := l.OnUnselected; f != nil && len(old) > 0 { + f(old[0]) + } + if f := l.OnSelected; f != nil { + f(id) + } + }() + l.scrollTo(id) + l.Refresh() +} + +// ScrollTo scrolls to the item represented by id +// +// Since: 2.1 +func (l *List) ScrollTo(id ListItemID) { + length := 0 + if f := l.Length; f != nil { + length = f() + } + if id < 0 || id >= length { + return + } + l.scrollTo(id) + l.Refresh() +} + +// ScrollToBottom scrolls to the end of the list +// +// Since: 2.1 +func (l *List) ScrollToBottom() { + l.scroller.ScrollToBottom() + l.offsetUpdated(l.scroller.Offset) +} + +// ScrollToTop scrolls to the start of the list +// +// Since: 2.1 +func (l *List) ScrollToTop() { + l.scroller.ScrollToTop() + l.offsetUpdated(l.scroller.Offset) +} + +// ScrollToOffset scrolls the list to the given offset position. +// +// Since: 2.5 +func (l *List) ScrollToOffset(offset float32) { + if l.scroller == nil { + return + } + if offset < 0 { + offset = 0 + } + contentHeight := l.contentMinSize().Height + if l.Size().Height >= contentHeight { + return // content fully visible - no need to scroll + } + if offset > contentHeight { + offset = contentHeight + } + l.scroller.ScrollToOffset(fyne.NewPos(0, offset)) + l.offsetUpdated(l.scroller.Offset) +} + +// GetScrollOffset returns the current scroll offset position +// +// Since: 2.5 +func (l *List) GetScrollOffset() float32 { + return l.offsetY +} + +// TypedKey is called if a key event happens while this List is focused. +func (l *List) TypedKey(event *fyne.KeyEvent) { + switch event.Name { + case fyne.KeySpace: + l.Select(l.currentFocus) + case fyne.KeyDown: + if f := l.Length; f != nil && l.currentFocus >= f()-1 { + return + } + l.RefreshItem(l.currentFocus) + l.currentFocus++ + l.scrollTo(l.currentFocus) + l.RefreshItem(l.currentFocus) + case fyne.KeyUp: + if l.currentFocus <= 0 { + return + } + l.RefreshItem(l.currentFocus) + l.currentFocus-- + l.scrollTo(l.currentFocus) + l.RefreshItem(l.currentFocus) + } +} + +// TypedRune is called if a text event happens while this List is focused. +func (l *List) TypedRune(_ rune) { + // intentionally left blank +} + +// Unselect removes the item identified by the given ID from the selection. +func (l *List) Unselect(id ListItemID) { + if len(l.selected) == 0 || l.selected[0] != id { + return + } + + l.selected = nil + l.Refresh() + if f := l.OnUnselected; f != nil { + f(id) + } +} + +// UnselectAll removes all items from the selection. +// +// Since: 2.1 +func (l *List) UnselectAll() { + if len(l.selected) == 0 { + return + } + + selected := l.selected + l.selected = nil + l.Refresh() + if f := l.OnUnselected; f != nil { + for _, id := range selected { + f(id) + } + } +} + +func (l *List) contentMinSize() fyne.Size { + separatorThickness := l.Theme().Size(theme.SizeNamePadding) + if l.Length == nil { + return fyne.NewSize(0, 0) + } + items := l.Length() + + if len(l.itemHeights) == 0 { + return fyne.NewSize(l.itemMin.Width, + (l.itemMin.Height+separatorThickness)*float32(items)-separatorThickness) + } + + height := float32(0) + totalCustom := 0 + templateHeight := l.itemMin.Height + for id, itemHeight := range l.itemHeights { + if id < items { + totalCustom++ + height += itemHeight + } + } + height += float32(items-totalCustom) * templateHeight + + return fyne.NewSize(l.itemMin.Width, height+separatorThickness*float32(items-1)) +} + +// fills l.visibleRowHeights and also returns offY and minRow +func (l *listLayout) calculateVisibleRowHeights(itemHeight float32, length int, th fyne.Theme) (offY float32, minRow int) { + rowOffset := float32(0) + isVisible := false + l.visibleRowHeights = l.visibleRowHeights[:0] + + if l.list.scroller.Size().Height <= 0 { + return offY, minRow + } + + padding := th.Size(theme.SizeNamePadding) + + if len(l.list.itemHeights) == 0 { + paddedItemHeight := itemHeight + padding + + offY = float32(math.Floor(float64(l.list.offsetY/paddedItemHeight))) * paddedItemHeight + minRow = int(math.Floor(float64(offY / paddedItemHeight))) + maxRow := int(math.Ceil(float64((offY + l.list.scroller.Size().Height) / paddedItemHeight))) + + if minRow > length-1 { + minRow = length - 1 + } + if minRow < 0 { + minRow = 0 + offY = 0 + } + + if maxRow > length-1 { + maxRow = length - 1 + } + + for i := 0; i <= maxRow-minRow; i++ { + l.visibleRowHeights = append(l.visibleRowHeights, itemHeight) + } + return offY, minRow + } + + for i := 0; i < length; i++ { + height := itemHeight + if h, ok := l.list.itemHeights[i]; ok { + height = h + } + + if rowOffset <= l.list.offsetY-height-padding { + // before scroll + } else if rowOffset <= l.list.offsetY { + minRow = i + offY = rowOffset + isVisible = true + } + if rowOffset >= l.list.offsetY+l.list.scroller.Size().Height { + break + } + + rowOffset += height + padding + if isVisible { + l.visibleRowHeights = append(l.visibleRowHeights, height) + } + } + return offY, minRow +} + +// Declare conformity with WidgetRenderer interface. +var _ fyne.WidgetRenderer = (*listRenderer)(nil) + +type listRenderer struct { + widget.BaseRenderer + + list *List + scroller *widget.Scroll + layout *fyne.Container +} + +func newListRenderer(objects []fyne.CanvasObject, l *List, scroller *widget.Scroll, layout *fyne.Container) *listRenderer { + lr := &listRenderer{BaseRenderer: widget.NewBaseRenderer(objects), list: l, scroller: scroller, layout: layout} + lr.scroller.OnScrolled = l.offsetUpdated + return lr +} + +func (l *listRenderer) Layout(size fyne.Size) { + l.scroller.Resize(size) +} + +func (l *listRenderer) MinSize() fyne.Size { + return l.scroller.MinSize().Max(l.list.itemMin) +} + +func (l *listRenderer) Refresh() { + if f := l.list.CreateItem; f != nil { + item := createItemAndApplyThemeScope(f, l.list) + l.list.itemMin = item.MinSize() + } + l.Layout(l.list.Size()) + l.scroller.Refresh() + layout := l.layout.Layout.(*listLayout) + layout.updateList(false) + + for _, s := range layout.separators { + s.Refresh() + } + canvas.Refresh(l.list.super()) +} + +// Declare conformity with interfaces. +var ( + _ fyne.Widget = (*listItem)(nil) + _ fyne.Tappable = (*listItem)(nil) + _ desktop.Hoverable = (*listItem)(nil) +) + +type listItem struct { + BaseWidget + + onTapped func() + background *canvas.Rectangle + child fyne.CanvasObject + hovered, selected bool +} + +func newListItem(child fyne.CanvasObject, tapped func()) *listItem { + li := &listItem{ + child: child, + onTapped: tapped, + } + + li.ExtendBaseWidget(li) + return li +} + +// CreateRenderer is a private method to Fyne which links this widget to its renderer. +func (li *listItem) CreateRenderer() fyne.WidgetRenderer { + li.ExtendBaseWidget(li) + th := li.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + li.background = canvas.NewRectangle(th.Color(theme.ColorNameHover, v)) + li.background.CornerRadius = th.Size(theme.SizeNameSelectionRadius) + li.background.Hide() + + objects := []fyne.CanvasObject{li.background, li.child} + + return &listItemRenderer{widget.NewBaseRenderer(objects), li} +} + +// MinSize returns the size that this widget should not shrink below. +func (li *listItem) MinSize() fyne.Size { + li.ExtendBaseWidget(li) + return li.BaseWidget.MinSize() +} + +// MouseIn is called when a desktop pointer enters the widget. +func (li *listItem) MouseIn(*desktop.MouseEvent) { + li.hovered = true + li.Refresh() +} + +// MouseMoved is called when a desktop pointer hovers over the widget. +func (li *listItem) MouseMoved(*desktop.MouseEvent) { +} + +// MouseOut is called when a desktop pointer exits the widget. +func (li *listItem) MouseOut() { + li.hovered = false + li.Refresh() +} + +// Tapped is called when a pointer tapped event is captured and triggers any tap handler. +func (li *listItem) Tapped(*fyne.PointEvent) { + if li.onTapped != nil { + li.selected = true + li.Refresh() + li.onTapped() + } +} + +// Declare conformity with the WidgetRenderer interface. +var _ fyne.WidgetRenderer = (*listItemRenderer)(nil) + +type listItemRenderer struct { + widget.BaseRenderer + + item *listItem +} + +// MinSize calculates the minimum size of a listItem. +// This is based on the size of the status indicator and the size of the child object. +func (li *listItemRenderer) MinSize() fyne.Size { + return li.item.child.MinSize() +} + +// Layout the components of the listItem widget. +func (li *listItemRenderer) Layout(size fyne.Size) { + li.item.background.Resize(size) + li.item.child.Resize(size) +} + +func (li *listItemRenderer) Refresh() { + th := li.item.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + li.item.background.CornerRadius = th.Size(theme.SizeNameSelectionRadius) + if li.item.selected { + li.item.background.FillColor = th.Color(theme.ColorNameSelection, v) + li.item.background.Show() + } else if li.item.hovered { + li.item.background.FillColor = th.Color(theme.ColorNameHover, v) + li.item.background.Show() + } else { + li.item.background.Hide() + } + li.item.background.Refresh() + canvas.Refresh(li.item.super()) +} + +// Declare conformity with Layout interface. +var _ fyne.Layout = (*listLayout)(nil) + +type listItemAndID struct { + item *listItem + id ListItemID +} + +type listLayout struct { + list *List + separators []fyne.CanvasObject + children []fyne.CanvasObject + + itemPool async.Pool[fyne.CanvasObject] + visible []listItemAndID + wasVisible []listItemAndID + visibleRowHeights []float32 +} + +func newListLayout(list *List) fyne.Layout { + l := &listLayout{list: list} + list.offsetUpdated = l.offsetUpdated + return l +} + +func (l *listLayout) Layout([]fyne.CanvasObject, fyne.Size) { + l.updateList(true) +} + +func (l *listLayout) MinSize([]fyne.CanvasObject) fyne.Size { + return l.list.contentMinSize() +} + +func (l *listLayout) getItem() *listItem { + item := l.itemPool.Get() + if item == nil { + if f := l.list.CreateItem; f != nil { + item2 := createItemAndApplyThemeScope(f, l.list) + + item = newListItem(item2, nil) + } + } + return item.(*listItem) +} + +func (l *listLayout) offsetUpdated(pos fyne.Position) { + if l.list.offsetY == pos.Y { + return + } + l.list.offsetY = pos.Y + l.updateList(true) +} + +func (l *listLayout) setupListItem(li *listItem, id ListItemID, focus bool) { + previousIndicator := li.selected + li.selected = false + for _, s := range l.list.selected { + if id == s { + li.selected = true + break + } + } + if focus { + li.hovered = true + li.Refresh() + } else if previousIndicator != li.selected || li.hovered { + li.hovered = false + li.Refresh() + } + if f := l.list.UpdateItem; f != nil { + f(id, li.child) + } + li.onTapped = func() { + if !fyne.CurrentDevice().IsMobile() { + canvas := fyne.CurrentApp().Driver().CanvasForObject(l.list) + if canvas != nil { + canvas.Focus(l.list.impl.(fyne.Focusable)) + } + + l.list.currentFocus = id + } + + l.list.Select(id) + } +} + +func (l *listLayout) updateList(newOnly bool) { + th := l.list.Theme() + separatorThickness := th.Size(theme.SizeNamePadding) + width := l.list.Size().Width + length := 0 + if f := l.list.Length; f != nil { + length = f() + } + if l.list.UpdateItem == nil { + fyne.LogError("Missing UpdateCell callback required for List", nil) + } + + // l.wasVisible now represents the currently visible items, while + // l.visible will be updated to represent what is visible *after* the update + l.wasVisible = append(l.wasVisible, l.visible...) + l.visible = l.visible[:0] + + offY, minRow := l.calculateVisibleRowHeights(l.list.itemMin.Height, length, th) + if len(l.visibleRowHeights) == 0 && length > 0 { // we can't show anything until we have some dimensions + return + } + + oldChildrenLen := len(l.children) + l.children = l.children[:0] + + y := offY + for index, itemHeight := range l.visibleRowHeights { + row := index + minRow + size := fyne.NewSize(width, itemHeight) + + c, ok := l.searchVisible(l.wasVisible, row) + if !ok { + c = l.getItem() + if c == nil { + continue + } + c.Resize(size) + } + + c.Move(fyne.NewPos(0, y)) + c.Resize(size) + + y += itemHeight + separatorThickness + l.visible = append(l.visible, listItemAndID{id: row, item: c}) + l.children = append(l.children, c) + } + l.nilOldSliceData(l.children, len(l.children), oldChildrenLen) + + for _, wasVis := range l.wasVisible { + if _, ok := l.searchVisible(l.visible, wasVis.id); !ok { + l.itemPool.Put(wasVis.item) + } + } + + l.updateSeparators() + + c := l.list.scroller.Content.(*fyne.Container) + oldObjLen := len(c.Objects) + c.Objects = c.Objects[:0] + c.Objects = append(c.Objects, l.children...) + c.Objects = append(c.Objects, l.separators...) + l.nilOldSliceData(c.Objects, len(c.Objects), oldObjLen) + + if newOnly { + for _, vis := range l.visible { + if _, ok := l.searchVisible(l.wasVisible, vis.id); !ok { + l.setupListItem(vis.item, vis.id, l.list.focused && l.list.currentFocus == vis.id) + } + } + } else { + for _, vis := range l.visible { + l.setupListItem(vis.item, vis.id, l.list.focused && l.list.currentFocus == vis.id) + } + + // a full refresh may change theme, we should drain the pool of unused items instead of refreshing them. + for l.itemPool.Get() != nil { + } + } + + // we don't need wasVisible now until next call to update + // nil out all references before truncating slice + for i := 0; i < len(l.wasVisible); i++ { + l.wasVisible[i].item = nil + } + l.wasVisible = l.wasVisible[:0] +} + +func (l *listLayout) updateSeparators() { + if l.list.HideSeparators { + l.separators = nil + return + } + if lenChildren := len(l.children); lenChildren > 1 { + if lenSep := len(l.separators); lenSep > lenChildren { + l.separators = l.separators[:lenChildren] + } else { + for i := lenSep; i < lenChildren; i++ { + + sep := NewSeparator() + if cache.OverrideThemeMatchingScope(sep, l.list) { + sep.Refresh() + } + + l.separators = append(l.separators, sep) + } + } + } else { + l.separators = nil + } + + th := l.list.Theme() + separatorThickness := th.Size(theme.SizeNameSeparatorThickness) + dividerOff := (th.Size(theme.SizeNamePadding) + separatorThickness) / 2 + for i, child := range l.children { + if i == 0 { + continue + } + l.separators[i].Move(fyne.NewPos(0, child.Position().Y-dividerOff)) + l.separators[i].Resize(fyne.NewSize(l.list.Size().Width, separatorThickness)) + l.separators[i].Show() + } +} + +// invariant: visible is in ascending order of IDs +func (l *listLayout) searchVisible(visible []listItemAndID, id ListItemID) (*listItem, bool) { + ln := len(visible) + idx := sort.Search(ln, func(i int) bool { return visible[i].id >= id }) + if idx < ln && visible[idx].id == id { + return visible[idx].item, true + } + return nil, false +} + +func (l *listLayout) nilOldSliceData(objs []fyne.CanvasObject, len, oldLen int) { + if oldLen > len { + objs = objs[:oldLen] // gain view into old data + for i := len; i < oldLen; i++ { + objs[i] = nil + } + } +} + +func createItemAndApplyThemeScope(f func() fyne.CanvasObject, scope fyne.Widget) fyne.CanvasObject { + item := f() + if !cache.OverrideThemeMatchingScope(item, scope) { + return item + } + + item.Refresh() + return item +} diff --git a/vendor/fyne.io/fyne/v2/widget/locale.go b/vendor/fyne.io/fyne/v2/widget/locale.go new file mode 100644 index 0000000..a4e3764 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/locale.go @@ -0,0 +1,120 @@ +package widget + +import ( + "strings" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/lang" +) + +type weekday int + +const ( + monday weekday = iota // default + sunday + saturday +) + +func (w weekday) String() string { + switch w { + case sunday: + return "Sunday" + case saturday: + return "Saturday" + default: + return "Monday" + } +} + +type localeSetting struct { + dateFormat string + weekStartDay weekday +} + +const defaultDateFormat = "02/01/2006" + +var localeSettings = map[string]localeSetting{ + "": { + dateFormat: defaultDateFormat, + weekStartDay: monday, + }, + "BR": { + weekStartDay: sunday, + }, + "BZ": { + weekStartDay: sunday, + }, + "CA": { + weekStartDay: sunday, + }, + "CO": { + weekStartDay: sunday, + }, + "DE": { + dateFormat: "02.01.2006", + }, + "DO": { + weekStartDay: sunday, + }, + "GT": { + weekStartDay: sunday, + }, + "JP": { + weekStartDay: sunday, + }, + "MX": { + weekStartDay: sunday, + }, + "NI": { + weekStartDay: sunday, + }, + "PE": { + weekStartDay: sunday, + }, + "PA": { + weekStartDay: sunday, + }, + "PY": { + weekStartDay: sunday, + }, + "SE": { + dateFormat: "2006-01-02", + }, + "US": { + dateFormat: "01/02/2006", + weekStartDay: sunday, + }, + "VE": { + weekStartDay: sunday, + }, + "ZA": { + weekStartDay: sunday, + }, +} + +func getLocaleDateFormat() string { + s := lookupLocaleSetting(lang.SystemLocale()) + if d := s.dateFormat; d != "" { + return d + } + return defaultDateFormat +} + +func getLocaleWeekStart() string { + s := lookupLocaleSetting(lang.SystemLocale()) + return s.weekStartDay.String() +} + +func lookupLocaleSetting(l fyne.Locale) localeSetting { + region := "" + lang := l.LanguageString() + if pos := strings.Index(lang, "-"); pos != -1 { + region = strings.Split(lang, "-")[1] + } + + if setting, ok := localeSettings[region]; ok { + return setting + } + + return localeSettings[""] +} diff --git a/vendor/fyne.io/fyne/v2/widget/markdown.go b/vendor/fyne.io/fyne/v2/widget/markdown.go new file mode 100644 index 0000000..c4f634d --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/markdown.go @@ -0,0 +1,192 @@ +package widget + +import ( + "io" + "net/url" + "strings" + + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/renderer" + + "fyne.io/fyne/v2" +) + +// NewRichTextFromMarkdown configures a RichText widget by parsing the provided markdown content. +// +// Since: 2.1 +func NewRichTextFromMarkdown(content string) *RichText { + return NewRichText(parseMarkdown(content)...) +} + +// ParseMarkdown allows setting the content of this RichText widget from a markdown string. +// It will replace the content of this widget similarly to SetText, but with the appropriate formatting. +func (t *RichText) ParseMarkdown(content string) { + t.Segments = parseMarkdown(content) + t.Refresh() +} + +// AppendMarkdown parses the given markdown string and appends the +// content to the widget, with the appropriate formatting. +// This API is intended for appending complete markdown documents or +// standalone fragments, and should not be used to parse a single +// markdown document piecewise. +// +// Since: 2.5 +func (t *RichText) AppendMarkdown(content string) { + t.Segments = append(t.Segments, parseMarkdown(content)...) + t.Refresh() +} + +type markdownRenderer []RichTextSegment + +func (m *markdownRenderer) AddOptions(...renderer.Option) {} + +func (m *markdownRenderer) Render(_ io.Writer, source []byte, n ast.Node) error { + segs, err := renderNode(source, n, false) + *m = segs + return err +} + +func renderNode(source []byte, n ast.Node, blockquote bool) ([]RichTextSegment, error) { + switch t := n.(type) { + case *ast.Document: + return renderChildren(source, n, blockquote) + case *ast.Paragraph: + children, err := renderChildren(source, n, blockquote) + if !blockquote { + linebreak := &TextSegment{Style: RichTextStyleParagraph} + children = append(children, linebreak) + } + return children, err + case *ast.List: + items, err := renderChildren(source, n, blockquote) + return []RichTextSegment{ + &ListSegment{startIndex: t.Start - 1, Items: items, Ordered: t.Marker != '*' && t.Marker != '-' && t.Marker != '+'}, + }, err + case *ast.ListItem: + texts, err := renderChildren(source, n, blockquote) + return []RichTextSegment{&ParagraphSegment{Texts: texts}}, err + case *ast.TextBlock: + return renderChildren(source, n, blockquote) + case *ast.Heading: + text := forceIntoHeadingText(source, n) + switch t.Level { + case 1: + return []RichTextSegment{&TextSegment{Style: RichTextStyleHeading, Text: text}}, nil + case 2: + return []RichTextSegment{&TextSegment{Style: RichTextStyleSubHeading, Text: text}}, nil + default: + textSegment := TextSegment{Style: RichTextStyleParagraph, Text: text} + textSegment.Style.TextStyle.Bold = true + return []RichTextSegment{&textSegment}, nil + } + case *ast.ThematicBreak: + return []RichTextSegment{&SeparatorSegment{}}, nil + case *ast.Link: + link, _ := url.Parse(string(t.Destination)) + text := forceIntoText(source, n) + return []RichTextSegment{&HyperlinkSegment{Alignment: fyne.TextAlignLeading, Text: text, URL: link}}, nil + case *ast.CodeSpan: + text := forceIntoText(source, n) + return []RichTextSegment{&TextSegment{Style: RichTextStyleCodeInline, Text: text}}, nil + case *ast.CodeBlock, *ast.FencedCodeBlock: + var data []byte + lines := n.Lines() + for i := 0; i < lines.Len(); i++ { + line := lines.At(i) + data = append(data, line.Value(source)...) + } + if len(data) == 0 { + return nil, nil + } + if data[len(data)-1] == '\n' { + data = data[:len(data)-1] + } + return []RichTextSegment{&TextSegment{Style: RichTextStyleCodeBlock, Text: string(data)}}, nil + case *ast.Emphasis: + text := forceIntoText(source, n) + switch t.Level { + case 2: + return []RichTextSegment{&TextSegment{Style: RichTextStyleStrong, Text: text}}, nil + default: + return []RichTextSegment{&TextSegment{Style: RichTextStyleEmphasis, Text: text}}, nil + } + case *ast.Text: + text := string(t.Value(source)) + if text == "" { + // These empty text elements indicate single line breaks after non-text elements in goldmark. + return []RichTextSegment{&TextSegment{Style: RichTextStyleInline, Text: " "}}, nil + } + text = suffixSpaceIfAppropriate(text, n) + if blockquote { + return []RichTextSegment{&TextSegment{Style: RichTextStyleBlockquote, Text: text}}, nil + } + return []RichTextSegment{&TextSegment{Style: RichTextStyleInline, Text: text}}, nil + case *ast.Blockquote: + return renderChildren(source, n, true) + case *ast.Image: + return parseMarkdownImage(t), nil + } + return nil, nil +} + +func suffixSpaceIfAppropriate(text string, n ast.Node) string { + next := n.NextSibling() + if next != nil && next.Type() == ast.TypeInline && !strings.HasSuffix(text, " ") { + return text + " " + } + return text +} + +func renderChildren(source []byte, n ast.Node, blockquote bool) ([]RichTextSegment, error) { + children := make([]RichTextSegment, 0, n.ChildCount()) + for childCount, child := n.ChildCount(), n.FirstChild(); childCount > 0; childCount-- { + segs, err := renderNode(source, child, blockquote) + if err != nil { + return children, err + } + children = append(children, segs...) + child = child.NextSibling() + } + return children, nil +} + +func forceIntoText(source []byte, n ast.Node) string { + text := strings.Builder{} + ast.Walk(n, func(n2 ast.Node, entering bool) (ast.WalkStatus, error) { + if entering { + switch t := n2.(type) { + case *ast.Text: + text.Write(t.Value(source)) + text.WriteByte(' ') + } + } + return ast.WalkContinue, nil + }) + return strings.TrimSuffix(text.String(), " ") +} + +func forceIntoHeadingText(source []byte, n ast.Node) string { + text := strings.Builder{} + ast.Walk(n, func(n2 ast.Node, entering bool) (ast.WalkStatus, error) { + if entering { + switch t := n2.(type) { + case *ast.Text: + text.Write(t.Value(source)) + } + } + return ast.WalkContinue, nil + }) + return text.String() +} + +func parseMarkdown(content string) []RichTextSegment { + r := markdownRenderer{} + md := goldmark.New(goldmark.WithRenderer(&r)) + err := md.Convert([]byte(content), nil) + if err != nil { + fyne.LogError("Failed to parse markdown", err) + } + return r +} diff --git a/vendor/fyne.io/fyne/v2/widget/markdown_image_notweb.go b/vendor/fyne.io/fyne/v2/widget/markdown_image_notweb.go new file mode 100644 index 0000000..f6e14b4 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/markdown_image_notweb.go @@ -0,0 +1,18 @@ +//go:build !wasm && !test_web_driver + +package widget + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/storage" + "github.com/yuin/goldmark/ast" +) + +func parseMarkdownImage(t *ast.Image) []RichTextSegment { + dest := string(t.Destination) + u, err := storage.ParseURI(dest) + if err != nil { + u = storage.NewFileURI(dest) + } + return []RichTextSegment{&ImageSegment{Source: u, Title: string(t.Title), Alignment: fyne.TextAlignCenter}} +} diff --git a/vendor/fyne.io/fyne/v2/widget/markdown_image_wasm.go b/vendor/fyne.io/fyne/v2/widget/markdown_image_wasm.go new file mode 100644 index 0000000..0cfbfcd --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/markdown_image_wasm.go @@ -0,0 +1,29 @@ +//go:build !ci && (!android || !ios || !mobile) && (wasm || test_web_driver) + +package widget + +import ( + "strings" + "syscall/js" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/storage" + "github.com/yuin/goldmark/ast" +) + +func parseMarkdownImage(t *ast.Image) []RichTextSegment { + dest := string(t.Destination) + u, err := storage.ParseURI(dest) + if err != nil { + if !strings.HasPrefix(dest, "/") { + dest = "/" + dest + } + origin := js.Global().Get("location").Get("origin").String() + u, err = storage.ParseURI(origin + dest) + if err != nil { + fyne.LogError("Can't load image in markdown", err) + return []RichTextSegment{} + } + } + return []RichTextSegment{&ImageSegment{Source: u, Title: string(t.Title), Alignment: fyne.TextAlignCenter}} +} diff --git a/vendor/fyne.io/fyne/v2/widget/menu.go b/vendor/fyne.io/fyne/v2/widget/menu.go new file mode 100644 index 0000000..bf3c7b6 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/menu.go @@ -0,0 +1,361 @@ +package widget + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/internal/widget" + "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/theme" +) + +var ( + _ fyne.Widget = (*Menu)(nil) + _ fyne.Tappable = (*Menu)(nil) +) + +// Menu is a widget for displaying a fyne.Menu. +type Menu struct { + BaseWidget + alignment fyne.TextAlign + Items []fyne.CanvasObject + OnDismiss func() `json:"-"` + activeItem *menuItem + customSized bool + containsCheck bool +} + +// NewMenu creates a new Menu. +func NewMenu(menu *fyne.Menu) *Menu { + m := &Menu{} + m.ExtendBaseWidget(m) + m.setMenu(menu) + return m +} + +// ActivateLastSubmenu finds the last active menu item traversing through the open submenus +// and activates its submenu if any. +// It returns `true` if there was a submenu and it was activated and `false` elsewhere. +// Activating a submenu does show it and activate its first item. +func (m *Menu) ActivateLastSubmenu() bool { + if m.activeItem == nil { + return false + } + if !m.activeItem.activateLastSubmenu() { + return false + } + m.Refresh() + return true +} + +// ActivateNext activates the menu item following the currently active menu item. +// If there is no menu item active, it activates the first menu item. +// If there is no menu item after the current active one, it does nothing. +// If a submenu is open, it delegates the activation to this submenu. +func (m *Menu) ActivateNext() { + if m.activeItem != nil && m.activeItem.isSubmenuOpen() { + m.activeItem.Child().ActivateNext() + return + } + + found := m.activeItem == nil + for _, item := range m.Items { + if mItem, ok := item.(*menuItem); ok { + if found { + m.activateItem(mItem) + return + } + if mItem == m.activeItem { + found = true + } + } + } +} + +// ActivatePrevious activates the menu item preceding the currently active menu item. +// If there is no menu item active, it activates the last menu item. +// If there is no menu item before the current active one, it does nothing. +// If a submenu is open, it delegates the activation to this submenu. +func (m *Menu) ActivatePrevious() { + if m.activeItem != nil && m.activeItem.isSubmenuOpen() { + m.activeItem.Child().ActivatePrevious() + return + } + + found := m.activeItem == nil + for i := len(m.Items) - 1; i >= 0; i-- { + item := m.Items[i] + if mItem, ok := item.(*menuItem); ok { + if found { + m.activateItem(mItem) + return + } + if mItem == m.activeItem { + found = true + } + } + } +} + +// CreateRenderer returns a new renderer for the menu. +func (m *Menu) CreateRenderer() fyne.WidgetRenderer { + m.ExtendBaseWidget(m) + box := newMenuBox(m.Items) + scroll := widget.NewVScroll(box) + scroll.SetMinSize(box.MinSize()) + objects := []fyne.CanvasObject{scroll} + for _, i := range m.Items { + if item, ok := i.(*menuItem); ok && item.Child() != nil { + objects = append(objects, item.Child()) + } + } + + return &menuRenderer{ + widget.NewShadowingRenderer(objects, widget.MenuLevel), + box, + m, + scroll, + } +} + +// DeactivateChild deactivates the active menu item and hides its submenu if any. +func (m *Menu) DeactivateChild() { + if m.activeItem != nil { + defer m.activeItem.Refresh() + if c := m.activeItem.Child(); c != nil { + c.Hide() + } + m.activeItem = nil + } +} + +// DeactivateLastSubmenu finds the last open submenu traversing through the open submenus, +// deactivates its active item and hides it. +// This also deactivates any submenus of the deactivated submenu. +// It returns `true` if there was a submenu open and closed and `false` elsewhere. +func (m *Menu) DeactivateLastSubmenu() bool { + if m.activeItem == nil { + return false + } + return m.activeItem.deactivateLastSubmenu() +} + +// MinSize returns the minimal size of the menu. +func (m *Menu) MinSize() fyne.Size { + m.ExtendBaseWidget(m) + return m.BaseWidget.MinSize() +} + +// Refresh updates the menu to reflect changes in the data. +func (m *Menu) Refresh() { + for _, item := range m.Items { + item.Refresh() + } + m.BaseWidget.Refresh() +} + +func (m *Menu) getContainsCheck() bool { + for _, item := range m.Items { + if mi, ok := item.(*menuItem); ok && mi.Item.Checked { + return true + } + } + return false +} + +// Tapped catches taps on separators and the menu background. It doesn't perform any action. +func (m *Menu) Tapped(*fyne.PointEvent) { + // Hit a separator or padding -> do nothing. +} + +// TriggerLast finds the last active menu item traversing through the open submenus and triggers it. +func (m *Menu) TriggerLast() { + if m.activeItem == nil { + m.Dismiss() + return + } + m.activeItem.triggerLast() +} + +// Dismiss dismisses the menu by dismissing and hiding the active child and performing OnDismiss. +func (m *Menu) Dismiss() { + if m.activeItem != nil { + if m.activeItem.Child() != nil { + defer m.activeItem.Child().Dismiss() + } + m.DeactivateChild() + } + if m.OnDismiss != nil { + m.OnDismiss() + } +} + +func (m *Menu) activateItem(item *menuItem) { + if item.Child() != nil { + item.Child().DeactivateChild() + } + if m.activeItem == item { + return + } + + m.DeactivateChild() + m.activeItem = item + m.activeItem.Refresh() + if m.activeItem.child != nil { + m.Refresh() + } +} + +func (m *Menu) setMenu(menu *fyne.Menu) { + m.Items = make([]fyne.CanvasObject, len(menu.Items)) + for i, item := range menu.Items { + if item.IsSeparator { + m.Items[i] = NewSeparator() + } else { + m.Items[i] = newMenuItem(item, m) + } + } + m.containsCheck = m.getContainsCheck() +} + +type menuRenderer struct { + *widget.ShadowingRenderer + box *menuBox + m *Menu + scroll *widget.Scroll +} + +func (r *menuRenderer) Layout(s fyne.Size) { + minSize := r.MinSize() + var boxSize fyne.Size + if r.m.customSized { + boxSize = minSize.Max(s) + } else { + boxSize = minSize + } + scrollSize := boxSize + + driver := fyne.CurrentApp().Driver() + if c := driver.CanvasForObject(r.m.super()); c != nil { + ap := driver.AbsolutePositionForObject(r.m.super()) + _, areaSize := c.InteractiveArea() + if ah := areaSize.Height - ap.Y; ah < boxSize.Height { + scrollSize = fyne.NewSize(boxSize.Width, ah) + } + } + if scrollSize != r.m.Size() { + r.m.Resize(scrollSize) + return + } + + r.LayoutShadow(scrollSize, fyne.NewPos(0, 0)) + r.scroll.Resize(scrollSize) + r.box.Resize(boxSize) + r.layoutActiveChild() +} + +func (r *menuRenderer) MinSize() fyne.Size { + return r.box.MinSize() +} + +func (r *menuRenderer) Refresh() { + r.layoutActiveChild() + r.ShadowingRenderer.RefreshShadow() + + for _, i := range r.m.Items { + if txt, ok := i.(*menuItem); ok { + txt.alignment = r.m.alignment + txt.Refresh() + } + } + + canvas.Refresh(r.m) +} + +func (r *menuRenderer) layoutActiveChild() { + item := r.m.activeItem + if item == nil || item.Child() == nil { + return + } + + if item.Child().Size().IsZero() { + item.Child().Resize(item.Child().MinSize()) + } + + itemSize := item.Size() + cp := fyne.NewPos(itemSize.Width, item.Position().Y) + d := fyne.CurrentApp().Driver() + c := d.CanvasForObject(item) + if c != nil { + absPos := d.AbsolutePositionForObject(item) + childSize := item.Child().Size() + if absPos.X+itemSize.Width+childSize.Width > c.Size().Width { + if absPos.X-childSize.Width >= 0 { + cp.X = -childSize.Width + } else { + cp.X = c.Size().Width - absPos.X - childSize.Width + } + } + requiredHeight := childSize.Height - r.m.Theme().Size(theme.SizeNamePadding) + availableHeight := c.Size().Height - absPos.Y + missingHeight := requiredHeight - availableHeight + if missingHeight > 0 { + cp.Y -= missingHeight + } + } + item.Child().Move(cp) +} + +type menuBox struct { + BaseWidget + items []fyne.CanvasObject +} + +var _ fyne.Widget = (*menuBox)(nil) + +func newMenuBox(items []fyne.CanvasObject) *menuBox { + b := &menuBox{items: items} + b.ExtendBaseWidget(b) + return b +} + +func (b *menuBox) CreateRenderer() fyne.WidgetRenderer { + th := b.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + background := canvas.NewRectangle(th.Color(theme.ColorNameMenuBackground, v)) + cont := &fyne.Container{Layout: layout.NewVBoxLayout(), Objects: b.items} + return &menuBoxRenderer{ + BaseRenderer: widget.NewBaseRenderer([]fyne.CanvasObject{background, cont}), + b: b, + background: background, + cont: cont, + } +} + +type menuBoxRenderer struct { + widget.BaseRenderer + b *menuBox + background *canvas.Rectangle + cont *fyne.Container +} + +var _ fyne.WidgetRenderer = (*menuBoxRenderer)(nil) + +func (r *menuBoxRenderer) Layout(size fyne.Size) { + s := fyne.NewSize(size.Width, size.Height) + r.background.Resize(s) + r.cont.Resize(s) +} + +func (r *menuBoxRenderer) MinSize() fyne.Size { + return r.cont.MinSize() +} + +func (r *menuBoxRenderer) Refresh() { + th := r.b.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + r.background.FillColor = th.Color(theme.ColorNameMenuBackground, v) + r.background.Refresh() + canvas.Refresh(r.b) +} diff --git a/vendor/fyne.io/fyne/v2/widget/menu_item.go b/vendor/fyne.io/fyne/v2/widget/menu_item.go new file mode 100644 index 0000000..4c4f75e --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/menu_item.go @@ -0,0 +1,404 @@ +package widget + +import ( + "image/color" + "strings" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/driver/desktop" + "fyne.io/fyne/v2/internal/svg" + "fyne.io/fyne/v2/internal/widget" + "fyne.io/fyne/v2/theme" +) + +var ( + _ fyne.Widget = (*menuItem)(nil) + _ desktop.Hoverable = (*menuItem)(nil) + _ fyne.Tappable = (*menuItem)(nil) +) + +// menuItem is a widget for displaying a fyne.menuItem. +type menuItem struct { + widget.Base + Item *fyne.MenuItem + + alignment fyne.TextAlign + child, parent *Menu +} + +// newMenuItem creates a new menuItem. +func newMenuItem(item *fyne.MenuItem, parent *Menu) *menuItem { + i := &menuItem{Item: item, parent: parent} + i.alignment = parent.alignment + i.ExtendBaseWidget(i) + return i +} + +func (i *menuItem) Child() *Menu { + if i.Item.ChildMenu != nil && i.child == nil { + child := NewMenu(i.Item.ChildMenu) + child.Hide() + child.OnDismiss = i.parent.Dismiss + i.child = child + } + return i.child +} + +// CreateRenderer returns a new renderer for the menu item. +func (i *menuItem) CreateRenderer() fyne.WidgetRenderer { + th := i.parent.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + background := canvas.NewRectangle(th.Color(theme.ColorNameHover, v)) + background.CornerRadius = th.Size(theme.SizeNameSelectionRadius) + background.Hide() + text := canvas.NewText(i.Item.Label, th.Color(theme.ColorNameForeground, v)) + text.Alignment = i.alignment + objects := []fyne.CanvasObject{background, text} + var expandIcon *canvas.Image + if i.Item.ChildMenu != nil { + expandIcon = canvas.NewImageFromResource(th.Icon(theme.IconNameMenuExpand)) + objects = append(objects, expandIcon) + } + checkIcon := canvas.NewImageFromResource(th.Icon(theme.IconNameConfirm)) + if !i.Item.Checked { + checkIcon.Hide() + } + var icon *canvas.Image + if i.Item.Icon != nil { + icon = canvas.NewImageFromResource(i.Item.Icon) + objects = append(objects, icon) + } + var shortcutTexts []*canvas.Text + if s, ok := i.Item.Shortcut.(fyne.KeyboardShortcut); ok { + shortcutTexts = textsForShortcut(s, th) + for _, t := range shortcutTexts { + objects = append(objects, t) + } + } + + objects = append(objects, checkIcon) + r := &menuItemRenderer{ + BaseRenderer: widget.NewBaseRenderer(objects), + i: i, + expandIcon: expandIcon, + checkIcon: checkIcon, + icon: icon, + shortcutTexts: shortcutTexts, + text: text, + background: background, + } + r.updateVisuals() + return r +} + +// MouseIn activates the item which shows the submenu if the item has one. +// The submenu of any sibling of the item will be hidden. +func (i *menuItem) MouseIn(*desktop.MouseEvent) { + i.activate() +} + +// MouseMoved does nothing. +func (i *menuItem) MouseMoved(*desktop.MouseEvent) { +} + +// MouseOut deactivates the item unless it has an open submenu. +func (i *menuItem) MouseOut() { + if !i.isSubmenuOpen() { + i.deactivate() + } +} + +// Tapped performs the action of the item and dismisses the menu. +// It does nothing if the item doesn’t have an action. +func (i *menuItem) Tapped(*fyne.PointEvent) { + if i.Item.Disabled { + return + } + if i.Item.Action == nil { + if fyne.CurrentDevice().IsMobile() { + i.activate() + } + + return + } else if i.Item.ChildMenu != nil { + if fyne.CurrentDevice().IsMobile() { + i.activate() + } + + return + } + + i.trigger() +} + +func (i *menuItem) activate() { + if i.Item.Disabled { + return + } + if i.Child() != nil { + i.Child().Show() + } + i.parent.activateItem(i) +} + +func (i *menuItem) activateLastSubmenu() bool { + if i.Child() == nil { + return false + } + if i.isSubmenuOpen() { + return i.Child().ActivateLastSubmenu() + } + i.Child().Show() + i.Child().ActivateNext() + return true +} + +func (i *menuItem) deactivate() { + if i.Child() != nil { + i.Child().Hide() + } + i.parent.DeactivateChild() +} + +func (i *menuItem) deactivateLastSubmenu() bool { + if !i.isSubmenuOpen() { + return false + } + if !i.Child().DeactivateLastSubmenu() { + i.Child().DeactivateChild() + i.Child().Hide() + } + return true +} + +func (i *menuItem) isActive() bool { + return i.parent.activeItem == i +} + +func (i *menuItem) isSubmenuOpen() bool { + return i.Child() != nil && i.Child().Visible() +} + +func (i *menuItem) trigger() { + i.parent.Dismiss() + if i.Item.Action != nil { + i.Item.Action() + } +} + +func (i *menuItem) triggerLast() { + if i.isSubmenuOpen() { + i.Child().TriggerLast() + return + } + i.trigger() +} + +type menuItemRenderer struct { + widget.BaseRenderer + i *menuItem + background *canvas.Rectangle + checkIcon *canvas.Image + expandIcon *canvas.Image + icon *canvas.Image + lastThemePadding float32 + minSize fyne.Size + shortcutTexts []*canvas.Text + text *canvas.Text +} + +func (r *menuItemRenderer) Layout(size fyne.Size) { + th := r.i.parent.Theme() + innerPad := th.Size(theme.SizeNameInnerPadding) + inlineIcon := th.Size(theme.SizeNameInlineIcon) + + leftOffset := innerPad + r.checkSpace() + rightOffset := size.Width + iconSize := fyne.NewSquareSize(inlineIcon) + iconTopOffset := (size.Height - inlineIcon) / 2 + + if r.expandIcon != nil { + rightOffset -= inlineIcon + r.expandIcon.Resize(iconSize) + r.expandIcon.Move(fyne.NewPos(rightOffset, iconTopOffset)) + } + + rightOffset -= innerPad + textHeight := r.text.MinSize().Height + for i := len(r.shortcutTexts) - 1; i >= 0; i-- { + text := r.shortcutTexts[i] + text.Resize(text.MinSize()) + rightOffset -= text.MinSize().Width + text.Move(fyne.NewPos(rightOffset, innerPad+(textHeight-text.Size().Height))) + + if i == 0 { + rightOffset -= innerPad + } + } + + r.checkIcon.Resize(iconSize) + r.checkIcon.Move(fyne.NewPos(innerPad, iconTopOffset)) + + if r.icon != nil { + r.icon.Resize(iconSize) + r.icon.Move(fyne.NewPos(leftOffset, iconTopOffset)) + leftOffset += inlineIcon + leftOffset += innerPad + } + + r.text.Resize(fyne.NewSize(rightOffset-leftOffset, textHeight)) + r.text.Move(fyne.NewPos(leftOffset, innerPad)) + + r.background.Resize(size) +} + +func (r *menuItemRenderer) MinSize() fyne.Size { + if r.minSizeUnchanged() { + return r.minSize + } + + th := r.i.parent.Theme() + innerPad := th.Size(theme.SizeNameInnerPadding) + inlineIcon := th.Size(theme.SizeNameInlineIcon) + innerPad2 := innerPad * 2 + + minSize := r.text.MinSize().AddWidthHeight(innerPad2+r.checkSpace(), innerPad2) + if r.expandIcon != nil { + minSize = minSize.AddWidthHeight(inlineIcon, 0) + } + if r.icon != nil { + minSize = minSize.AddWidthHeight(inlineIcon+innerPad, 0) + } + if r.shortcutTexts != nil { + var textWidth float32 + for _, text := range r.shortcutTexts { + textWidth += text.MinSize().Width + } + minSize = minSize.AddWidthHeight(textWidth+innerPad, 0) + } + r.minSize = minSize + return r.minSize +} + +func (r *menuItemRenderer) updateVisuals() { + th := r.i.parent.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + r.background.CornerRadius = th.Size(theme.SizeNameSelectionRadius) + if fyne.CurrentDevice().IsMobile() { + r.background.Hide() + } else if r.i.isActive() { + r.background.FillColor = th.Color(theme.ColorNameFocus, v) + r.background.Show() + } else { + r.background.Hide() + } + r.background.Refresh() + r.text.Alignment = r.i.alignment + r.refreshText(r.text, false) + for _, text := range r.shortcutTexts { + r.refreshText(text, true) + } + + if r.i.Item.Checked { + r.checkIcon.Show() + } else { + r.checkIcon.Hide() + } + r.updateIcon(r.checkIcon, th.Icon(theme.IconNameConfirm)) + r.updateIcon(r.expandIcon, th.Icon(theme.IconNameMenuExpand)) + r.updateIcon(r.icon, r.i.Item.Icon) +} + +func (r *menuItemRenderer) Refresh() { + r.updateVisuals() + canvas.Refresh(r.i) +} + +func (r *menuItemRenderer) checkSpace() float32 { + if r.i.parent.containsCheck { + return theme.IconInlineSize() + theme.InnerPadding() + } + return 0 +} + +func (r *menuItemRenderer) minSizeUnchanged() bool { + th := r.i.parent.Theme() + + return !r.minSize.IsZero() && + r.text.TextSize == th.Size(theme.SizeNameText) && + (r.expandIcon == nil || r.expandIcon.Size().Width == th.Size(theme.SizeNameInlineIcon)) && + r.lastThemePadding == th.Size(theme.SizeNameInnerPadding) +} + +func (r *menuItemRenderer) updateIcon(img *canvas.Image, rsc fyne.Resource) { + if img == nil { + return + } + if r.i.Item.Disabled && svg.IsResourceSVG(rsc) { + img.Resource = theme.NewDisabledResource(rsc) + } else { + img.Resource = rsc + } +} + +func (r *menuItemRenderer) refreshText(text *canvas.Text, shortcut bool) { + th := r.i.parent.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + text.TextSize = th.Size(theme.SizeNameText) + if r.i.Item.Disabled { + text.Color = th.Color(theme.ColorNameDisabled, v) + } else { + if shortcut { + text.Color = shortcutColor(th) + } else { + text.Color = th.Color(theme.ColorNameForeground, v) + } + } + text.Refresh() +} + +func shortcutColor(th fyne.Theme) color.Color { + v := fyne.CurrentApp().Settings().ThemeVariant() + r, g, b, a := th.Color(theme.ColorNameForeground, v).RGBA() + a = uint32(float32(a) * 0.95) + return color.NRGBA{R: uint8(r), G: uint8(g), B: uint8(b), A: uint8(a)} +} + +func textsForShortcut(sc fyne.KeyboardShortcut, th fyne.Theme) (texts []*canvas.Text) { + // add modifier + b := strings.Builder{} + mods := sc.Mod() + if mods&fyne.KeyModifierControl != 0 { + b.WriteString(textModifierControl) + } + if mods&fyne.KeyModifierAlt != 0 { + b.WriteString(textModifierAlt) + } + if mods&fyne.KeyModifierShift != 0 { + b.WriteString(textModifierShift) + } + if mods&fyne.KeyModifierSuper != 0 { + b.WriteString(textModifierSuper) + } + shortColor := shortcutColor(th) + if b.Len() > 0 { + t := canvas.NewText(b.String(), shortColor) + t.TextStyle = styleModifiers + texts = append(texts, t) + } + // add key + style := defaultStyleKeys + s, ok := keyTexts[sc.Key()] + if !ok { + s = string(sc.Key()) + } else if len(s) == 1 { + style = fyne.TextStyle{Symbol: true} + } + t := canvas.NewText(s, shortColor) + t.TextStyle = style + texts = append(texts, t) + return texts +} diff --git a/vendor/fyne.io/fyne/v2/widget/menu_item_darwin.go b/vendor/fyne.io/fyne/v2/widget/menu_item_darwin.go new file mode 100644 index 0000000..099d2fb --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/menu_item_darwin.go @@ -0,0 +1,35 @@ +package widget + +import ( + "fyne.io/fyne/v2" +) + +const ( + textModifierAlt = "⌥" + textModifierControl = "⌃" + textModifierShift = "⇧" + textModifierSuper = "⌘" +) + +var ( + styleModifiers = fyne.TextStyle{Symbol: true} + defaultStyleKeys = fyne.TextStyle{Monospace: true} +) + +var keyTexts = map[fyne.KeyName]string{ + fyne.KeyBackspace: "⌫", + fyne.KeyDelete: "⌦", + fyne.KeyDown: "↓", + fyne.KeyEnd: "↘", + fyne.KeyEnter: "↩", + fyne.KeyEscape: "⎋", + fyne.KeyHome: "↖", + fyne.KeyLeft: "←", + fyne.KeyPageDown: "⇟", + fyne.KeyPageUp: "⇞", + fyne.KeyReturn: "↩", + fyne.KeyRight: "→", + fyne.KeySpace: "␣", + fyne.KeyTab: "⇥", + fyne.KeyUp: "↑", +} diff --git a/vendor/fyne.io/fyne/v2/widget/menu_item_other.go b/vendor/fyne.io/fyne/v2/widget/menu_item_other.go new file mode 100644 index 0000000..6a63f64 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/menu_item_other.go @@ -0,0 +1,37 @@ +//go:build !darwin + +package widget + +import ( + "fyne.io/fyne/v2" +) + +const ( + textModifierAlt = "Alt+" + textModifierControl = "Ctrl+" + textModifierShift = "Shift+" + textModifierSuper = "Super+" +) + +var ( + styleModifiers = fyne.TextStyle{} + defaultStyleKeys = fyne.TextStyle{} +) + +var keyTexts = map[fyne.KeyName]string{ + fyne.KeyBackspace: "Backspace", + fyne.KeyDelete: "Del", + fyne.KeyDown: "↓", + fyne.KeyEnd: "End", + fyne.KeyEnter: "Enter", + fyne.KeyEscape: "Esc", + fyne.KeyHome: "Home", + fyne.KeyLeft: "←", + fyne.KeyPageDown: "PgDn", + fyne.KeyPageUp: "PgUp", + fyne.KeyReturn: "Return", + fyne.KeyRight: "→", + fyne.KeySpace: "Space", + fyne.KeyTab: "Tab", + fyne.KeyUp: "↑", +} diff --git a/vendor/fyne.io/fyne/v2/widget/popup.go b/vendor/fyne.io/fyne/v2/widget/popup.go new file mode 100644 index 0000000..d49ec38 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/popup.go @@ -0,0 +1,305 @@ +package widget + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/internal/widget" + "fyne.io/fyne/v2/theme" +) + +var _ fyne.Widget = (*PopUp)(nil) + +// PopUp is a widget that can float above the user interface. +// It wraps any standard elements with padding and a shadow. +// If it is modal then the shadow will cover the entire canvas it hovers over and block interactions. +type PopUp struct { + BaseWidget + + Content fyne.CanvasObject + Canvas fyne.Canvas + + innerPos fyne.Position + innerSize fyne.Size + modal bool + overlayShown bool +} + +// Hide this widget, if it was previously visible +func (p *PopUp) Hide() { + if p.overlayShown { + p.Canvas.Overlays().Remove(p) + p.overlayShown = false + } + p.BaseWidget.Hide() +} + +// Move the widget to a new position. A PopUp position is absolute to the top, left of its canvas. +// For PopUp this actually moves the content so checking Position() will not return the same value as is set here. +func (p *PopUp) Move(pos fyne.Position) { + if p.modal { + return + } + p.innerPos = pos + p.Refresh() +} + +// Resize changes the size of the PopUp's content. +// PopUps always have the size of their canvas, but this call updates the +// size of the content portion. +func (p *PopUp) Resize(size fyne.Size) { + p.innerSize = size + // The canvas size might not have changed and therefore the Resize won't trigger a layout. + // Until we have a widget.Relayout() or similar, the renderer's refresh will do the re-layout. + p.Refresh() +} + +// Show this pop-up as overlay if not already shown. +func (p *PopUp) Show() { + if !p.overlayShown { + p.Canvas.Overlays().Add(p) + p.overlayShown = true + } + p.Refresh() + p.BaseWidget.Show() +} + +// ShowAtPosition shows this pop-up at the given position. +func (p *PopUp) ShowAtPosition(pos fyne.Position) { + p.Move(pos) + p.Show() +} + +// ShowAtRelativePosition shows this pop-up at the given position relative to stated object. +// +// Since 2.4 +func (p *PopUp) ShowAtRelativePosition(rel fyne.Position, to fyne.CanvasObject) { + withRelativePosition(rel, to, p.ShowAtPosition) +} + +// Tapped is called when the user taps the popUp. +// If not modal and the tap is outside the content area, then dismiss this widget +func (p *PopUp) Tapped(e *fyne.PointEvent) { + if !p.modal && !p.isInsideContent(e.Position) { + p.Hide() + } +} + +// TappedSecondary is called when the user right/alt taps the popUp. +// If not modal and the tap is outside the content area, then dismiss this widget +func (p *PopUp) TappedSecondary(e *fyne.PointEvent) { + if !p.modal && !p.isInsideContent(e.Position) { + p.Hide() + } +} + +// MinSize returns the size that this widget should not shrink below +func (p *PopUp) MinSize() fyne.Size { + p.ExtendBaseWidget(p) + return p.BaseWidget.MinSize() +} + +// CreateRenderer is a private method to Fyne which links this widget to its renderer +func (p *PopUp) CreateRenderer() fyne.WidgetRenderer { + th := p.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + p.ExtendBaseWidget(p) + background := canvas.NewRectangle(th.Color(theme.ColorNameOverlayBackground, v)) + if p.modal { + underlay := canvas.NewRectangle(th.Color(theme.ColorNameShadow, v)) + objects := []fyne.CanvasObject{underlay, background, p.Content} + return &modalPopUpRenderer{ + widget.NewShadowingRenderer(objects, widget.DialogLevel), + popUpBaseRenderer{popUp: p, background: background}, + underlay, + } + } + objects := []fyne.CanvasObject{background, p.Content} + return &popUpRenderer{ + widget.NewShadowingRenderer(objects, widget.PopUpLevel), + popUpBaseRenderer{popUp: p, background: background}, + } +} + +func (p *PopUp) isInsideContent(pos fyne.Position) bool { + return pos.X >= p.innerPos.X && pos.Y >= p.innerPos.Y && + pos.X <= p.innerPos.X+p.innerSize.Width && + pos.Y <= p.innerPos.Y+p.innerSize.Height +} + +// ShowPopUpAtPosition creates a new popUp for the specified content at the specified absolute position. +// It will then display the popup on the passed canvas. +func ShowPopUpAtPosition(content fyne.CanvasObject, canvas fyne.Canvas, pos fyne.Position) { + newPopUp(content, canvas).ShowAtPosition(pos) +} + +// ShowPopUpAtRelativePosition shows a new popUp for the specified content at the given position relative to stated object. +// It will then display the popup on the passed canvas. +// +// Since 2.4 +func ShowPopUpAtRelativePosition(content fyne.CanvasObject, canvas fyne.Canvas, rel fyne.Position, to fyne.CanvasObject) { + withRelativePosition(rel, to, func(pos fyne.Position) { + ShowPopUpAtPosition(content, canvas, pos) + }) +} + +func newPopUp(content fyne.CanvasObject, canvas fyne.Canvas) *PopUp { + ret := &PopUp{Content: content, Canvas: canvas, modal: false} + ret.ExtendBaseWidget(ret) + return ret +} + +// NewPopUp creates a new popUp for the specified content and displays it on the passed canvas. +func NewPopUp(content fyne.CanvasObject, canvas fyne.Canvas) *PopUp { + return newPopUp(content, canvas) +} + +// ShowPopUp creates a new popUp for the specified content and displays it on the passed canvas. +func ShowPopUp(content fyne.CanvasObject, canvas fyne.Canvas) { + newPopUp(content, canvas).Show() +} + +func newModalPopUp(content fyne.CanvasObject, canvas fyne.Canvas) *PopUp { + p := &PopUp{Content: content, Canvas: canvas, modal: true} + p.ExtendBaseWidget(p) + return p +} + +// NewModalPopUp creates a new popUp for the specified content and displays it on the passed canvas. +// A modal PopUp blocks interactions with underlying elements, covered with a semi-transparent overlay. +func NewModalPopUp(content fyne.CanvasObject, canvas fyne.Canvas) *PopUp { + return newModalPopUp(content, canvas) +} + +// ShowModalPopUp creates a new popUp for the specified content and displays it on the passed canvas. +// A modal PopUp blocks interactions with underlying elements, covered with a semi-transparent overlay. +func ShowModalPopUp(content fyne.CanvasObject, canvas fyne.Canvas) { + p := newModalPopUp(content, canvas) + p.Show() +} + +type popUpBaseRenderer struct { + popUp *PopUp + background *canvas.Rectangle +} + +func (r *popUpBaseRenderer) padding() fyne.Size { + th := r.popUp.Theme() + return fyne.NewSquareSize(th.Size(theme.SizeNameInnerPadding)) +} + +func (r *popUpBaseRenderer) offset() fyne.Position { + th := r.popUp.Theme() + return fyne.NewSquareOffsetPos(th.Size(theme.SizeNameInnerPadding) / 2) +} + +type popUpRenderer struct { + *widget.ShadowingRenderer + popUpBaseRenderer +} + +func (r *popUpRenderer) Layout(_ fyne.Size) { + innerSize := r.popUp.innerSize.Max(r.popUp.MinSize()) + r.popUp.Content.Resize(innerSize.Subtract(r.padding())) + + innerPos := r.popUp.innerPos + if innerPos.X+innerSize.Width > r.popUp.Canvas.Size().Width { + innerPos.X = r.popUp.Canvas.Size().Width - innerSize.Width + if innerPos.X < 0 { + innerPos.X = 0 // TODO here we may need a scroller as it's wider than our canvas + } + } + if innerPos.Y+innerSize.Height > r.popUp.Canvas.Size().Height { + innerPos.Y = r.popUp.Canvas.Size().Height - innerSize.Height + if innerPos.Y < 0 { + innerPos.Y = 0 // TODO here we may need a scroller as it's longer than our canvas + } + } + r.popUp.Content.Move(innerPos.Add(r.offset())) + + r.background.Resize(innerSize) + r.background.Move(innerPos) + r.LayoutShadow(innerSize, innerPos) +} + +func (r *popUpRenderer) MinSize() fyne.Size { + return r.popUp.Content.MinSize().Add(r.padding()) +} + +func (r *popUpRenderer) Refresh() { + th := r.popUp.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + r.background.FillColor = th.Color(theme.ColorNameOverlayBackground, v) + expectedContentSize := r.popUp.innerSize.Max(r.popUp.MinSize()).Subtract(r.padding()) + shouldRelayout := r.popUp.Content.Size() != expectedContentSize + + if r.background.Size() != r.popUp.innerSize || r.background.Position() != r.popUp.innerPos || shouldRelayout { + r.Layout(r.popUp.Size()) + } + if r.popUp.Canvas.Size() != r.popUp.BaseWidget.Size() { + r.popUp.BaseWidget.Resize(r.popUp.Canvas.Size()) + } + r.popUp.Content.Refresh() + r.background.Refresh() + r.ShadowingRenderer.RefreshShadow() +} + +type modalPopUpRenderer struct { + *widget.ShadowingRenderer + popUpBaseRenderer + underlay *canvas.Rectangle +} + +func (r *modalPopUpRenderer) Layout(canvasSize fyne.Size) { + r.underlay.Resize(canvasSize) + + padding := r.padding() + innerSize := r.popUp.innerSize.Max(r.popUp.Content.MinSize().Add(padding)) + + requestedSize := innerSize.Subtract(padding) + size := r.popUp.Content.MinSize().Max(requestedSize) + size = size.Min(canvasSize.Subtract(padding)) + pos := fyne.NewPos((canvasSize.Width-size.Width)/2, (canvasSize.Height-size.Height)/2) + r.popUp.Content.Move(pos) + r.popUp.Content.Resize(size) + + innerPos := pos.Subtract(r.offset()) + r.background.Move(innerPos) + r.background.Resize(size.Add(padding)) + r.LayoutShadow(innerSize, innerPos) +} + +func (r *modalPopUpRenderer) MinSize() fyne.Size { + return r.popUp.Content.MinSize().Add(r.padding()) +} + +func (r *modalPopUpRenderer) Refresh() { + th := r.popUp.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + r.underlay.FillColor = th.Color(theme.ColorNameShadow, v) + r.background.FillColor = th.Color(theme.ColorNameOverlayBackground, v) + expectedContentSize := r.popUp.innerSize.Max(r.popUp.MinSize()).Subtract(r.padding()) + shouldLayout := r.popUp.Content.Size() != expectedContentSize + + if r.background.Size() != r.popUp.innerSize || shouldLayout { + r.Layout(r.popUp.Size()) + } + if r.popUp.Canvas.Size() != r.popUp.BaseWidget.Size() { + r.popUp.BaseWidget.Resize(r.popUp.Canvas.Size()) + } + r.popUp.Content.Refresh() + r.background.Refresh() +} + +func withRelativePosition(rel fyne.Position, to fyne.CanvasObject, f func(position fyne.Position)) { + d := fyne.CurrentApp().Driver() + c := d.CanvasForObject(to) + if c == nil { + fyne.LogError("Could not locate parent object to display relative to", nil) + f(rel) + return + } + + pos := d.AbsolutePositionForObject(to).Add(rel) + f(pos) +} diff --git a/vendor/fyne.io/fyne/v2/widget/popup_menu.go b/vendor/fyne.io/fyne/v2/widget/popup_menu.go new file mode 100644 index 0000000..e5b64b1 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/popup_menu.go @@ -0,0 +1,143 @@ +package widget + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/internal/widget" +) + +var ( + _ fyne.Widget = (*PopUpMenu)(nil) + _ fyne.Focusable = (*PopUpMenu)(nil) +) + +// PopUpMenu is a Menu which displays itself in an OverlayContainer. +type PopUpMenu struct { + *Menu + canvas fyne.Canvas + overlay *widget.OverlayContainer +} + +// NewPopUpMenu creates a new, reusable popup menu. You can show it using ShowAtPosition. +// +// Since: 2.0 +func NewPopUpMenu(menu *fyne.Menu, c fyne.Canvas) *PopUpMenu { + m := &Menu{} + m.setMenu(menu) + p := &PopUpMenu{Menu: m, canvas: c} + p.ExtendBaseWidget(p) + p.Menu.Resize(p.Menu.MinSize()) + p.Menu.customSized = true + o := widget.NewOverlayContainer(p, c, p.Dismiss) + o.Resize(o.MinSize()) + p.overlay = o + p.OnDismiss = func() { + p.Hide() + } + return p +} + +// ShowPopUpMenuAtPosition creates a PopUp menu populated with items from the passed menu structure. +// It will automatically be positioned at the provided location and shown as an overlay on the specified canvas. +func ShowPopUpMenuAtPosition(menu *fyne.Menu, c fyne.Canvas, pos fyne.Position) { + m := NewPopUpMenu(menu, c) + m.ShowAtPosition(pos) +} + +// ShowPopUpMenuAtRelativePosition creates a PopUp menu populated with menu items from the passed menu structure. +// It will automatically be positioned at the given position relative to stated object and shown as an overlay on the specified canvas. +// +// Since 2.4 +func ShowPopUpMenuAtRelativePosition(menu *fyne.Menu, c fyne.Canvas, rel fyne.Position, to fyne.CanvasObject) { + withRelativePosition(rel, to, func(pos fyne.Position) { + ShowPopUpMenuAtPosition(menu, c, pos) + }) +} + +// FocusGained is triggered when the object gained focus. For the pop-up menu it does nothing. +func (p *PopUpMenu) FocusGained() {} + +// FocusLost is triggered when the object lost focus. For the pop-up menu it does nothing. +func (p *PopUpMenu) FocusLost() {} + +// Hide hides the pop-up menu. +func (p *PopUpMenu) Hide() { + p.overlay.Hide() + p.Menu.Hide() +} + +// Move moves the pop-up menu. +// The position is absolute because pop-up menus are shown in an overlay which covers the whole canvas. +func (p *PopUpMenu) Move(pos fyne.Position) { + p.BaseWidget.Move(p.adjustedPosition(pos, p.Size())) +} + +// Resize changes the size of the pop-up menu. +func (p *PopUpMenu) Resize(size fyne.Size) { + p.BaseWidget.Move(p.adjustedPosition(p.Position(), size)) + p.Menu.Resize(size) +} + +// Show makes the pop-up menu visible. +func (p *PopUpMenu) Show() { + p.Menu.alignment = p.alignment + p.Menu.Refresh() + + p.overlay.Show() + p.Menu.Show() + if !fyne.CurrentDevice().IsMobile() { + p.canvas.Focus(p) + } +} + +// ShowAtPosition shows the pop-up menu at the specified position. +func (p *PopUpMenu) ShowAtPosition(pos fyne.Position) { + p.Move(pos) + p.Show() +} + +// ShowAtRelativePosition shows the pop-up menu at the position relative to given object. +// +// Since 2.4 +func (p *PopUpMenu) ShowAtRelativePosition(rel fyne.Position, to fyne.CanvasObject) { + withRelativePosition(rel, to, p.ShowAtPosition) +} + +// TypedKey handles key events. It allows keyboard control of the pop-up menu. +func (p *PopUpMenu) TypedKey(e *fyne.KeyEvent) { + switch e.Name { + case fyne.KeyDown: + p.ActivateNext() + case fyne.KeyEnter, fyne.KeyReturn, fyne.KeySpace: + p.TriggerLast() + case fyne.KeyEscape: + p.Dismiss() + case fyne.KeyLeft: + p.DeactivateLastSubmenu() + case fyne.KeyRight: + p.ActivateLastSubmenu() + case fyne.KeyUp: + p.ActivatePrevious() + } +} + +// TypedRune handles text events. For pop-up menus this does nothing. +func (p *PopUpMenu) TypedRune(rune) {} + +func (p *PopUpMenu) adjustedPosition(pos fyne.Position, size fyne.Size) fyne.Position { + x := pos.X + y := pos.Y + _, areaSize := p.canvas.InteractiveArea() + if x+size.Width > areaSize.Width { + x = areaSize.Width - size.Width + if x < 0 { + x = 0 // TODO here we may need a scroller as it's wider than our canvas + } + } + if y+size.Height > areaSize.Height { + y = areaSize.Height - size.Height + if y < 0 { + y = 0 // TODO here we may need a scroller as it's longer than our canvas + } + } + return fyne.NewPos(x, y) +} diff --git a/vendor/fyne.io/fyne/v2/widget/progressbar.go b/vendor/fyne.io/fyne/v2/widget/progressbar.go new file mode 100644 index 0000000..f655bff --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/progressbar.go @@ -0,0 +1,198 @@ +package widget + +import ( + "image/color" + "strconv" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/data/binding" + col "fyne.io/fyne/v2/internal/color" + "fyne.io/fyne/v2/internal/widget" + "fyne.io/fyne/v2/theme" +) + +type progressRenderer struct { + widget.BaseRenderer + background, bar canvas.Rectangle + label canvas.Text + ratio float32 + progress *ProgressBar +} + +// MinSize calculates the minimum size of a progress bar. +// This is simply the "100%" label size plus padding. +func (p *progressRenderer) MinSize() fyne.Size { + text := "100%" + if format := p.progress.TextFormatter; format != nil { + text = format() + } + + th := p.progress.Theme() + padding := th.Size(theme.SizeNameInnerPadding) * 2 + size := fyne.MeasureText(text, p.label.TextSize, p.label.TextStyle) + return size.AddWidthHeight(padding, padding) +} + +func (p *progressRenderer) calculateRatio() { + if p.progress.Value < p.progress.Min { + p.progress.Value = p.progress.Min + } + if p.progress.Value > p.progress.Max { + p.progress.Value = p.progress.Max + } + + delta := p.progress.Max - p.progress.Min + p.ratio = float32((p.progress.Value - p.progress.Min) / delta) +} + +func (p *progressRenderer) updateBar() { + p.Layout(p.progress.Size()) // Make sure that bar length updates. + + // Don't draw rectangles when they can't be seen. + p.background.Hidden = p.ratio == 1.0 + p.bar.Hidden = p.ratio == 0.0 + + if text := p.progress.TextFormatter; text != nil { + p.label.Text = text() + return + } + + p.label.Text = strconv.Itoa(int(p.ratio*100)) + "%" +} + +// Layout the components of the check widget +func (p *progressRenderer) Layout(size fyne.Size) { + p.calculateRatio() + + p.bar.Resize(fyne.NewSize(size.Width*p.ratio, size.Height)) + p.background.Resize(size) + p.label.Resize(size) +} + +// applyTheme updates the progress bar to match the current theme +func (p *progressRenderer) applyTheme() { + th := p.progress.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + primaryColor := th.Color(theme.ColorNamePrimary, v) + inputRadius := th.Size(theme.SizeNameInputRadius) + + p.background.FillColor = progressBlendColor(primaryColor) + p.background.CornerRadius = inputRadius + p.bar.FillColor = primaryColor + p.bar.CornerRadius = inputRadius + p.label.Color = th.Color(theme.ColorNameForegroundOnPrimary, v) + p.label.TextSize = th.Size(theme.SizeNameText) +} + +func (p *progressRenderer) Refresh() { + p.applyTheme() + p.updateBar() + p.background.Refresh() + p.bar.Refresh() + p.label.Refresh() + canvas.Refresh(p.progress.super()) +} + +// ProgressBar widget creates a horizontal panel that indicates progress +type ProgressBar struct { + BaseWidget + + Min, Max, Value float64 + + // TextFormatter can be used to have a custom format of progress text. + // If set, it overrides the percentage readout and runs each time the value updates. + // + // Since: 1.4 + TextFormatter func() string `json:"-"` + + binder basicBinder +} + +// Bind connects the specified data source to this ProgressBar. +// The current value will be displayed and any changes in the data will cause the widget to update. +// +// Since: 2.0 +func (p *ProgressBar) Bind(data binding.Float) { + p.binder.SetCallback(p.updateFromData) + p.binder.Bind(data) +} + +// SetValue changes the current value of this progress bar (from p.Min to p.Max). +// The widget will be refreshed to indicate the change. +func (p *ProgressBar) SetValue(v float64) { + p.Value = v + p.Refresh() +} + +// MinSize returns the size that this widget should not shrink below +func (p *ProgressBar) MinSize() fyne.Size { + p.ExtendBaseWidget(p) + return p.BaseWidget.MinSize() +} + +// CreateRenderer is a private method to Fyne which links this widget to its renderer +func (p *ProgressBar) CreateRenderer() fyne.WidgetRenderer { + p.ExtendBaseWidget(p) + if p.Min == 0 && p.Max == 0 { + p.Max = 1.0 + } + + renderer := &progressRenderer{progress: p} + renderer.label.Alignment = fyne.TextAlignCenter + renderer.applyTheme() + renderer.updateBar() + + renderer.SetObjects([]fyne.CanvasObject{&renderer.background, &renderer.bar, &renderer.label}) + return renderer +} + +// Unbind disconnects any configured data source from this ProgressBar. +// The current value will remain at the last value of the data source. +// +// Since: 2.0 +func (p *ProgressBar) Unbind() { + p.binder.Unbind() +} + +// NewProgressBar creates a new progress bar widget. +// The default Min is 0 and Max is 1, Values set should be between those numbers. +// The display will convert this to a percentage. +func NewProgressBar() *ProgressBar { + bar := &ProgressBar{Min: 0, Max: 1} + bar.ExtendBaseWidget(bar) + return bar +} + +// NewProgressBarWithData returns a progress bar connected with the specified data source. +// +// Since: 2.0 +func NewProgressBarWithData(data binding.Float) *ProgressBar { + p := NewProgressBar() + p.Bind(data) + return p +} + +func progressBlendColor(clr color.Color) color.Color { + r, g, b, a := col.ToNRGBA(clr) + faded := uint8(a) / 2 + return &color.NRGBA{R: uint8(r), G: uint8(g), B: uint8(b), A: faded} +} + +func (p *ProgressBar) updateFromData(data binding.DataItem) { + if data == nil { + return + } + floatSource, ok := data.(binding.Float) + if !ok { + return + } + + val, err := floatSource.Get() + if err != nil { + fyne.LogError("Error getting current data value", err) + return + } + p.SetValue(val) +} diff --git a/vendor/fyne.io/fyne/v2/widget/progressbarinfinite.go b/vendor/fyne.io/fyne/v2/widget/progressbarinfinite.go new file mode 100644 index 0000000..389699f --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/progressbarinfinite.go @@ -0,0 +1,201 @@ +package widget + +import ( + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/internal/widget" + "fyne.io/fyne/v2/theme" +) + +const ( + infiniteRefreshRate = 50 * time.Millisecond + maxProgressBarInfiniteWidthRatio = 1.0 / 5 + minProgressBarInfiniteWidthRatio = 1.0 / 20 + progressBarInfiniteStepSizeRatio = 1.0 / 50 +) + +type infProgressRenderer struct { + widget.BaseRenderer + background, bar canvas.Rectangle + animation fyne.Animation + wasRunning bool + progress *ProgressBarInfinite +} + +// MinSize calculates the minimum size of a progress bar. +func (p *infProgressRenderer) MinSize() fyne.Size { + th := p.progress.Theme() + innerPad2 := th.Size(theme.SizeNameInnerPadding) * 2 + // this is to create the same size infinite progress bar as regular progress bar + text := fyne.MeasureText("100%", th.Size(theme.SizeNameText), fyne.TextStyle{}) + + return fyne.NewSize(text.Width+innerPad2, text.Height+innerPad2) +} + +func (p *infProgressRenderer) updateBar(done float32) { + size := p.progress.Size() + progressWidth := size.Width + spanWidth := progressWidth + (progressWidth * (maxProgressBarInfiniteWidthRatio / 2)) + maxBarWidth := progressWidth * maxProgressBarInfiniteWidthRatio + + barCenterX := spanWidth*done - (spanWidth-progressWidth)/2 + barPos := fyne.NewPos(barCenterX-maxBarWidth/2, 0) + barSize := fyne.NewSize(maxBarWidth, size.Height) + if barPos.X < 0 { + barSize.Width += barPos.X + barPos.X = 0 + } + if barPos.X+barSize.Width > progressWidth { + barSize.Width = progressWidth - barPos.X + } + + p.bar.Resize(barSize) + p.bar.Move(barPos) + canvas.Refresh(&p.bar) +} + +// Layout the components of the infinite progress bar +func (p *infProgressRenderer) Layout(size fyne.Size) { + p.background.Resize(size) +} + +// Refresh updates the size and position of the horizontal scrolling infinite progress bar +func (p *infProgressRenderer) Refresh() { + running := p.progress.Running() + if running { + if !p.wasRunning { + p.start() + } + return // we refresh from the goroutine + } else if p.wasRunning { + p.stop() + return + } + + th := p.progress.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + cornerRadius := th.Size(theme.SizeNameInputRadius) + primaryColor := th.Color(theme.ColorNamePrimary, v) + + p.background.FillColor = progressBlendColor(primaryColor) + p.background.CornerRadius = cornerRadius + p.bar.FillColor = primaryColor + p.bar.CornerRadius = cornerRadius + p.background.Refresh() + p.bar.Refresh() + canvas.Refresh(p.progress.super()) +} + +// Start the infinite progress bar background thread to update it continuously +func (p *infProgressRenderer) start() { + p.animation.Duration = time.Second * 3 + p.animation.Tick = p.updateBar + p.animation.Curve = fyne.AnimationLinear + p.animation.RepeatCount = fyne.AnimationRepeatForever + p.animation.AutoReverse = true + + p.wasRunning = true + p.animation.Start() +} + +// Stop the background thread from updating the infinite progress bar +func (p *infProgressRenderer) stop() { + p.wasRunning = false + p.animation.Stop() +} + +func (p *infProgressRenderer) Destroy() { + p.progress.running = false + + p.stop() +} + +// ProgressBarInfinite widget creates a horizontal panel that indicates waiting indefinitely +// An infinite progress bar loops 0% -> 100% repeatedly until Stop() is called +type ProgressBarInfinite struct { + BaseWidget + running bool +} + +// Show this widget, if it was previously hidden +func (p *ProgressBarInfinite) Show() { + p.running = true + + p.BaseWidget.Show() +} + +// Hide this widget, if it was previously visible +func (p *ProgressBarInfinite) Hide() { + p.running = false + + p.BaseWidget.Hide() +} + +// Start the infinite progress bar animation +func (p *ProgressBarInfinite) Start() { + if p.running { + return + } + + p.running = true + p.BaseWidget.Refresh() +} + +// Stop the infinite progress bar animation +func (p *ProgressBarInfinite) Stop() { + if !p.running { + return + } + + p.running = false + p.BaseWidget.Refresh() +} + +// Running returns the current state of the infinite progress animation +func (p *ProgressBarInfinite) Running() bool { + return p.running +} + +// MinSize returns the size that this widget should not shrink below +func (p *ProgressBarInfinite) MinSize() fyne.Size { + p.ExtendBaseWidget(p) + return p.BaseWidget.MinSize() +} + +// CreateRenderer is a private method to Fyne which links this widget to its renderer +func (p *ProgressBarInfinite) CreateRenderer() fyne.WidgetRenderer { + p.ExtendBaseWidget(p) + th := p.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + primaryColor := th.Color(theme.ColorNamePrimary, v) + cornerRadius := th.Size(theme.SizeNameInputRadius) + + render := &infProgressRenderer{ + background: canvas.Rectangle{ + FillColor: progressBlendColor(primaryColor), + CornerRadius: cornerRadius, + }, + bar: canvas.Rectangle{ + FillColor: primaryColor, + CornerRadius: cornerRadius, + }, + progress: p, + } + + render.SetObjects([]fyne.CanvasObject{&render.background, &render.bar}) + + p.running = true + return render +} + +// NewProgressBarInfinite creates a new progress bar widget that loops indefinitely from 0% -> 100% +// SetValue() is not defined for infinite progress bar +// To stop the looping progress and set the progress bar to 100%, call ProgressBarInfinite.Stop() +func NewProgressBarInfinite() *ProgressBarInfinite { + bar := &ProgressBarInfinite{} + bar.ExtendBaseWidget(bar) + return bar +} diff --git a/vendor/fyne.io/fyne/v2/widget/radio_group.go b/vendor/fyne.io/fyne/v2/widget/radio_group.go new file mode 100644 index 0000000..702c8b7 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/radio_group.go @@ -0,0 +1,248 @@ +package widget + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/internal/widget" +) + +// RadioGroup widget has a list of text labels and checks check icons next to each. +// Changing the selection (only one can be selected) will trigger the changed func. +// +// Since: 1.4 +type RadioGroup struct { + DisableableWidget + Horizontal bool + Required bool + OnChanged func(string) `json:"-"` + Options []string + Selected string + + // this index is ONE-BASED so the default zero-value is unselected + // use r.selectedIndex(), r.setSelectedIndex(int) to maniupulate this field + // as if it were a zero-based index (with -1 == nothing selected) + _selIdx int +} + +var _ fyne.Widget = (*RadioGroup)(nil) + +// NewRadioGroup creates a new radio group widget with the set options and change handler +// +// Since: 1.4 +func NewRadioGroup(options []string, changed func(string)) *RadioGroup { + r := &RadioGroup{ + Options: options, + OnChanged: changed, + } + r.ExtendBaseWidget(r) + return r +} + +// Append adds a new option to the end of a RadioGroup widget. +func (r *RadioGroup) Append(option string) { + r.Options = append(r.Options, option) + + r.Refresh() +} + +// CreateRenderer is a private method to Fyne which links this widget to its renderer +func (r *RadioGroup) CreateRenderer() fyne.WidgetRenderer { + r.ExtendBaseWidget(r) + + items := make([]fyne.CanvasObject, len(r.Options)) + for i, option := range r.Options { + idx := i + items[idx] = newRadioItem(option, func(item *radioItem) { + r.itemTapped(item, idx) + }) + } + + render := &radioGroupRenderer{widget.NewBaseRenderer(items), items, r} + r.updateSelectedIndex() + render.updateItems(false) + return render +} + +// MinSize returns the size that this widget should not shrink below +func (r *RadioGroup) MinSize() fyne.Size { + r.ExtendBaseWidget(r) + return r.BaseWidget.MinSize() +} + +// SetSelected sets the radio option, it can be used to set a default option. +func (r *RadioGroup) SetSelected(option string) { + if r.Selected == option { + return + } + + r.Selected = option + // selectedIndex will be updated on refresh to the first matching item + + if r.OnChanged != nil { + r.OnChanged(option) + } + + r.Refresh() +} + +func (r *RadioGroup) itemTapped(item *radioItem, idx int) { + if r.Disabled() { + return + } + + if r.selectedIndex() == idx { + if r.Required { + return + } + r.Selected = "" + r.setSelectedIndex(-1) + item.SetSelected(false) + } else { + r.Selected = item.Label + r.setSelectedIndex(idx) + item.SetSelected(true) + } + + if r.OnChanged != nil { + r.OnChanged(r.Selected) + } + r.Refresh() +} + +func (r *RadioGroup) Refresh() { + r.updateSelectedIndex() + r.BaseWidget.Refresh() +} + +func (r *RadioGroup) selectedIndex() int { + return r._selIdx - 1 +} + +func (r *RadioGroup) setSelectedIndex(idx int) { + r._selIdx = idx + 1 +} + +// if selectedIndex does not match the public Selected property, +// set it to the index of the first radio item whose label matches Selected +func (r *RadioGroup) updateSelectedIndex() { + sel := r.Selected + sIdx := r.selectedIndex() + if sIdx >= 0 && sIdx < len(r.Options) && r.Options[sIdx] == sel { + return // selected index matches Selected + } + if sIdx == -1 && sel == "" { + return // nothing selected + } + + sIdx = -1 + for i, opt := range r.Options { + if sel == opt { + sIdx = i + break + } + } + r.setSelectedIndex(sIdx) +} + +type radioGroupRenderer struct { + widget.BaseRenderer + + // slice of *radioItem, but using fyne.CanvasObject as the type + // so we can directly set it to the BaseRenderer's objects slice + items []fyne.CanvasObject + radio *RadioGroup +} + +// Layout the components of the radio widget +func (r *radioGroupRenderer) Layout(_ fyne.Size) { + count := 1 + if len(r.items) > 0 { + count = len(r.items) + } + var itemHeight, itemWidth float32 + minSize := r.radio.MinSize() + if r.radio.Horizontal { + itemHeight = minSize.Height + itemWidth = minSize.Width / float32(count) + } else { + itemHeight = minSize.Height / float32(count) + itemWidth = minSize.Width + } + + itemSize := fyne.NewSize(itemWidth, itemHeight) + x, y := float32(0), float32(0) + for _, item := range r.items { + item.Resize(itemSize) + item.Move(fyne.NewPos(x, y)) + if r.radio.Horizontal { + x += itemWidth + } else { + y += itemHeight + } + } +} + +// MinSize calculates the minimum size of a radio item. +// This is based on the contained text, the radio icon and a standard amount of padding +// between each item. +func (r *radioGroupRenderer) MinSize() fyne.Size { + width := float32(0) + height := float32(0) + for _, item := range r.items { + itemMin := item.MinSize() + + width = fyne.Max(width, itemMin.Width) + height = fyne.Max(height, itemMin.Height) + } + + if r.radio.Horizontal { + width = width * float32(len(r.items)) + } else { + height = height * float32(len(r.items)) + } + + return fyne.NewSize(width, height) +} + +func (r *radioGroupRenderer) Refresh() { + r.updateItems(true) + canvas.Refresh(r.radio.super()) +} + +func (r *radioGroupRenderer) updateItems(refresh bool) { + if len(r.items) < len(r.radio.Options) { + for i := len(r.items); i < len(r.radio.Options); i++ { + idx := i + item := newRadioItem(r.radio.Options[idx], func(item *radioItem) { + r.radio.itemTapped(item, idx) + }) + r.items = append(r.items, item) + } + r.Layout(r.radio.Size()) + } else if len(r.items) > len(r.radio.Options) { + total := len(r.radio.Options) + r.items = r.items[:total] + } + r.SetObjects(r.items) + + for i, item := range r.items { + item := item.(*radioItem) + changed := false + if l := r.radio.Options[i]; l != item.Label { + item.Label = r.radio.Options[i] + changed = true + } + if sel := i == r.radio.selectedIndex(); sel != item.Selected { + item.Selected = sel + changed = true + } + if d := r.radio.Disabled(); d != item.Disabled() { + item.disabled = d + changed = true + } + + if refresh || changed { + item.Refresh() + } + } +} diff --git a/vendor/fyne.io/fyne/v2/widget/radio_item.go b/vendor/fyne.io/fyne/v2/widget/radio_item.go new file mode 100644 index 0000000..abaf4de --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/radio_item.go @@ -0,0 +1,207 @@ +package widget + +import ( + "image/color" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/driver/desktop" + "fyne.io/fyne/v2/internal/widget" + "fyne.io/fyne/v2/theme" +) + +var ( + _ fyne.Widget = (*radioItem)(nil) + _ desktop.Hoverable = (*radioItem)(nil) + _ fyne.Tappable = (*radioItem)(nil) + _ fyne.Focusable = (*radioItem)(nil) +) + +func newRadioItem(label string, onTap func(*radioItem)) *radioItem { + i := &radioItem{Label: label, onTap: onTap} + i.ExtendBaseWidget(i) + return i +} + +// radioItem is a single radio item to be used by RadioGroup. +type radioItem struct { + DisableableWidget + + Label string + Selected bool + + focused bool + hovered bool + onTap func(item *radioItem) +} + +// CreateRenderer is a private method to Fyne which links this widget to its renderer. +func (i *radioItem) CreateRenderer() fyne.WidgetRenderer { + txt := canvas.Text{Alignment: fyne.TextAlignLeading} + txt.TextSize = i.Theme().Size(theme.SizeNameText) + r := &radioItemRenderer{item: i, label: &txt} + r.SetObjects([]fyne.CanvasObject{&r.focusIndicator, &r.icon, &r.over, &txt}) + r.update() + return r +} + +// FocusGained is called when this item gained the focus. +func (i *radioItem) FocusGained() { + i.focused = true + i.Refresh() +} + +// FocusLost is called when this item lost the focus. +func (i *radioItem) FocusLost() { + i.focused = false + i.Refresh() +} + +// MouseIn is called when a desktop pointer enters the widget. +func (i *radioItem) MouseIn(_ *desktop.MouseEvent) { + if i.Disabled() { + return + } + + i.hovered = true + i.Refresh() +} + +// MouseMoved is called when a desktop pointer hovers over the widget. +func (i *radioItem) MouseMoved(_ *desktop.MouseEvent) { +} + +// MouseOut is called when a desktop pointer exits the widget +func (i *radioItem) MouseOut() { + if i.Disabled() { + return + } + + i.hovered = false + i.Refresh() +} + +// SetSelected sets whether this radio item is selected or not. +func (i *radioItem) SetSelected(selected bool) { + if i.Disabled() || i.Selected == selected { + return + } + + i.Selected = selected + i.Refresh() +} + +// Tapped is called when a pointer tapped event is captured and triggers any change handler +func (i *radioItem) Tapped(_ *fyne.PointEvent) { + if !i.focused { + focusIfNotMobile(i.super()) + } + i.toggle() +} + +// TypedKey is called when this item receives a key event. +func (i *radioItem) TypedKey(_ *fyne.KeyEvent) { +} + +// TypedRune is called when this item receives a char event. +func (i *radioItem) TypedRune(r rune) { + if r == ' ' { + i.toggle() + } +} + +func (i *radioItem) toggle() { + if i.Disabled() || i.onTap == nil { + return + } + + i.onTap(i) +} + +type radioItemRenderer struct { + widget.BaseRenderer + item *radioItem + + focusIndicator canvas.Circle + icon, over canvas.Image + label *canvas.Text +} + +func (r *radioItemRenderer) Layout(size fyne.Size) { + th := r.item.Theme() + innerPadding := th.Size(theme.SizeNameInnerPadding) + borderSize := th.Size(theme.SizeNameInputBorder) + iconInlineSize := th.Size(theme.SizeNameInlineIcon) + + focusIndicatorSize := fyne.NewSquareSize(iconInlineSize + innerPadding) + r.focusIndicator.Resize(focusIndicatorSize) + r.focusIndicator.Move(fyne.NewPos(borderSize, (size.Height-focusIndicatorSize.Height)/2)) + + labelSize := fyne.NewSize(size.Width, size.Height) + r.label.Resize(labelSize) + r.label.Move(fyne.NewPos(focusIndicatorSize.Width+th.Size(theme.SizeNamePadding), 0)) + + iconPos := fyne.NewPos(innerPadding/2+borderSize, (size.Height-iconInlineSize)/2) + iconSize := fyne.NewSquareSize(iconInlineSize) + r.icon.Resize(iconSize) + r.icon.Move(iconPos) + r.over.Resize(iconSize) + r.over.Move(iconPos) +} + +func (r *radioItemRenderer) MinSize() fyne.Size { + th := r.item.Theme() + inPad := th.Size(theme.SizeNameInnerPadding) * 2 + + return r.label.MinSize(). + AddWidthHeight(inPad+th.Size(theme.SizeNameInlineIcon)+th.Size(theme.SizeNamePadding), inPad) +} + +func (r *radioItemRenderer) Refresh() { + r.update() + canvas.Refresh(r.item.super()) +} + +func (r *radioItemRenderer) update() { + th := r.item.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + r.label.Text = r.item.Label + r.label.TextSize = th.Size(theme.SizeNameText) + if r.item.Disabled() { + r.label.Color = th.Color(theme.ColorNameDisabled, v) + } else { + r.label.Color = th.Color(theme.ColorNameForeground, v) + } + + out := theme.NewThemedResource(th.Icon(theme.IconNameRadioButton)) + out.ColorName = theme.ColorNameInputBorder + in := theme.NewThemedResource(th.Icon(theme.IconNameRadioButtonFill)) + in.ColorName = theme.ColorNameInputBackground + if r.item.Selected { + in.ColorName = theme.ColorNamePrimary + out.ColorName = theme.ColorNameForeground + } + if r.item.Disabled() { + if r.item.Selected { + in.ColorName = theme.ColorNameDisabled + } else { + in.ColorName = theme.ColorNameBackground + } + out.ColorName = theme.ColorNameDisabled + } + r.icon.Resource = in + r.icon.Refresh() + r.over.Resource = out + r.over.Refresh() + + if r.item.Disabled() { + r.focusIndicator.FillColor = color.Transparent + } else if r.item.focused { + r.focusIndicator.FillColor = th.Color(theme.ColorNameFocus, v) + } else if r.item.hovered { + r.focusIndicator.FillColor = th.Color(theme.ColorNameHover, v) + } else { + r.focusIndicator.FillColor = color.Transparent + } +} diff --git a/vendor/fyne.io/fyne/v2/widget/richtext.go b/vendor/fyne.io/fyne/v2/widget/richtext.go new file mode 100644 index 0000000..8b0fd64 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/richtext.go @@ -0,0 +1,1142 @@ +package widget + +import ( + "image/color" + "math" + "strings" + "unicode" + + "github.com/go-text/typesetting/di" + "github.com/go-text/typesetting/shaping" + "golang.org/x/image/math/fixed" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/internal/cache" + paint "fyne.io/fyne/v2/internal/painter" + "fyne.io/fyne/v2/internal/widget" + "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/theme" +) + +const passwordChar = "•" + +var _ fyne.Widget = (*RichText)(nil) + +// RichText represents the base element for a rich text-based widget. +// +// Since: 2.1 +type RichText struct { + BaseWidget + Segments []RichTextSegment + Wrapping fyne.TextWrap + Scroll fyne.ScrollDirection + + // The truncation mode of the text + // + // Since: 2.4 + Truncation fyne.TextTruncation + + inset fyne.Size // this varies due to how the widget works (entry with scroller vs others with padding) + rowBounds []rowBoundary // cache for boundaries + scr *widget.Scroll + prop *canvas.Rectangle // used to apply text minsize to the scroller `scr`, if present - TODO improve #2464 + + visualCache map[RichTextSegment][]fyne.CanvasObject + minCache fyne.Size +} + +// NewRichText returns a new RichText widget that renders the given text and segments. +// If no segments are specified it will be converted to a single segment using the default text settings. +// +// Since: 2.1 +func NewRichText(segments ...RichTextSegment) *RichText { + t := &RichText{Segments: segments} + t.Scroll = widget.ScrollNone + return t +} + +// NewRichTextWithText returns a new RichText widget that renders the given text. +// The string will be converted to a single text segment using the default text settings. +// +// Since: 2.1 +func NewRichTextWithText(text string) *RichText { + return NewRichText(&TextSegment{ + Style: RichTextStyleInline, + Text: text, + }) +} + +// CreateRenderer is a private method to Fyne which links this widget to its renderer +func (t *RichText) CreateRenderer() fyne.WidgetRenderer { + t.prop = canvas.NewRectangle(color.Transparent) + if t.scr == nil && t.Scroll != widget.ScrollNone { + t.scr = widget.NewScroll(&fyne.Container{Layout: layout.NewStackLayout(), Objects: []fyne.CanvasObject{ + t.prop, &fyne.Container{}, + }}) + } + + t.ExtendBaseWidget(t) + r := &textRenderer{obj: t} + + t.updateRowBounds() // set up the initial text layout etc + r.Refresh() + return r +} + +// MinSize calculates the minimum size of a rich text widget. +// This is based on the contained text with a standard amount of padding added. +func (t *RichText) MinSize() fyne.Size { + // we don't return the minCache here, as any internal segments could have caused it to change... + t.ExtendBaseWidget(t) + + min := t.BaseWidget.MinSize() + t.minCache = min + return min +} + +// Refresh triggers a redraw of the rich text. +func (t *RichText) Refresh() { + t.minCache = fyne.Size{} + t.updateRowBounds() + + for _, s := range t.Segments { + if txt, ok := s.(*TextSegment); ok { + txt.parent = t + } + } + + t.BaseWidget.Refresh() +} + +// Resize sets a new size for the rich text. +// This should only be called if it is not in a container with a layout manager. +func (t *RichText) Resize(size fyne.Size) { + if size == t.Size() { + return + } + + t.size = size + + skipResize := !t.minCache.IsZero() && size.Width >= t.minCache.Width && size.Height >= t.minCache.Height && t.Wrapping == fyne.TextWrapOff && t.Truncation == fyne.TextTruncateOff + + if skipResize { + if len(t.Segments) < 2 { // we can simplify :) + cache.Renderer(t).Layout(size) + return + } + } + t.updateRowBounds() + + t.Refresh() +} + +// String returns the text widget buffer as string +func (t *RichText) String() string { + ret := strings.Builder{} + for _, seg := range t.Segments { + ret.WriteString(seg.Textual()) + } + return ret.String() +} + +// charMinSize returns the average char size to use for internal computation +func (t *RichText) charMinSize(concealed bool, style fyne.TextStyle, textSize float32) fyne.Size { + defaultChar := "M" + if concealed { + defaultChar = passwordChar + } + + return fyne.MeasureText(defaultChar, textSize, style) +} + +// deleteFromTo removes the text between the specified positions +func (t *RichText) deleteFromTo(lowBound int, highBound int) []rune { + if lowBound >= highBound { + return []rune{} + } + + start := 0 + ret := make([]rune, 0, highBound-lowBound) + deleting := false + var segs []RichTextSegment + for i, seg := range t.Segments { + if _, ok := seg.(*TextSegment); !ok { + if !deleting { + segs = append(segs, seg) + } + continue + } + end := start + len([]rune(seg.(*TextSegment).Text)) + if end < lowBound { + segs = append(segs, seg) + start = end + continue + } + + startOff := int(math.Max(float64(lowBound-start), 0)) + endOff := int(math.Min(float64(end), float64(highBound))) - start + r := ([]rune)(seg.(*TextSegment).Text) + ret = append(ret, r[startOff:endOff]...) + r2 := append(r[:startOff], r[endOff:]...) + seg.(*TextSegment).Text = string(r2) + segs = append(segs, seg) + + // prepare next iteration + start = end + if start >= highBound { + segs = append(segs, t.Segments[i+1:]...) + break + } else if start >= lowBound { + deleting = true + } + } + t.Segments = segs + t.Refresh() + return ret +} + +// cachedSegmentVisual returns a cached segment visual representation. +// The offset value is > 0 if the segment had been split and so we need multiple objects. +func (t *RichText) cachedSegmentVisual(seg RichTextSegment, offset int) fyne.CanvasObject { + if t.visualCache == nil { + t.visualCache = make(map[RichTextSegment][]fyne.CanvasObject) + } + + if vis, ok := t.visualCache[seg]; ok && offset < len(vis) { + return vis[offset] + } + + vis := seg.Visual() + if offset < len(t.visualCache[seg]) { + t.visualCache[seg][offset] = vis + } else { + t.visualCache[seg] = append(t.visualCache[seg], vis) + } + return vis +} + +func (t *RichText) cleanVisualCache() { + if len(t.visualCache) <= len(t.Segments) { + return + } + var deletingSegs []RichTextSegment + for seg1 := range t.visualCache { + found := false + for _, seg2 := range t.Segments { + if seg1 == seg2 { + found = true + break + } + } + if !found { + // cached segment is not currently in t.Segments, clear it + deletingSegs = append(deletingSegs, seg1) + } + } + for _, seg := range deletingSegs { + delete(t.visualCache, seg) + } +} + +// insertAt inserts the text at the specified position +func (t *RichText) insertAt(pos int, runes []rune) { + index := 0 + start := 0 + var into *TextSegment + for i, seg := range t.Segments { + if _, ok := seg.(*TextSegment); !ok { + continue + } + end := start + len([]rune(seg.(*TextSegment).Text)) + into = seg.(*TextSegment) + index = i + if end > pos { + break + } + + start = end + } + + if into == nil { + return + } + r := ([]rune)(into.Text) + if pos > len(r) { // safety in case position is out of bounds for the segment + pos = len(r) + } + r2 := append(r[:pos], append(runes, r[pos:]...)...) + into.Text = string(r2) + t.Segments[index] = into +} + +// Len returns the text widget buffer length +func (t *RichText) len() int { + ret := 0 + for _, seg := range t.Segments { + ret += len([]rune(seg.Textual())) + } + return ret +} + +// lineSizeToColumn returns the rendered size for the line specified by row up to the col position +func (t *RichText) lineSizeToColumn(col, row int, textSize, innerPad float32) fyne.Size { + if row < 0 { + row = 0 + } + if col < 0 { + col = 0 + } + bound := t.rowBoundary(row) + total := fyne.NewSize(0, 0) + counted := 0 + last := false + if bound == nil { + return t.charMinSize(false, fyne.TextStyle{}, textSize) + } + for i, seg := range bound.segments { + var size fyne.Size + if text, ok := seg.(*TextSegment); ok { + start := 0 + if i == 0 { + start = bound.begin + } + measureText := []rune(text.Text)[start:] + if col < counted+len(measureText) { + measureText = measureText[0 : col-counted] + last = true + } + if concealed(seg) { + measureText = []rune(strings.Repeat(passwordChar, len(measureText))) + } + counted += len(measureText) + + label := canvas.NewText(string(measureText), color.Black) + label.TextStyle = text.Style.TextStyle + label.TextSize = text.size() + + size = label.MinSize() + } else { + size = t.cachedSegmentVisual(seg, 0).MinSize() + } + + total.Width += size.Width + total.Height = fyne.Max(total.Height, size.Height) + if last { + break + } + } + return total.Add(fyne.NewSize(innerPad-t.inset.Width, 0)) +} + +// Row returns the characters in the row specified. +// The row parameter should be between 0 and t.Rows()-1. +func (t *RichText) row(row int) []rune { + if row < 0 || row >= t.rows() { + return nil + } + bound := t.rowBounds[row] + var ret []rune + for i, seg := range bound.segments { + if text, ok := seg.(*TextSegment); ok { + if i == 0 { + if len(bound.segments) == 1 { + ret = append(ret, []rune(text.Text)[bound.begin:bound.end]...) + } else { + ret = append(ret, []rune(text.Text)[bound.begin:]...) + } + } else if i == len(bound.segments)-1 && len(bound.segments) > 1 && bound.end != 0 { + ret = append(ret, []rune(text.Text)[:bound.end]...) + } + } + } + return ret +} + +// RowBoundary returns the boundary of the row specified. +// The row parameter should be between 0 and t.Rows()-1. +func (t *RichText) rowBoundary(row int) *rowBoundary { + if row < 0 || row >= t.rows() { + return nil + } + return &t.rowBounds[row] +} + +// RowLength returns the number of visible characters in the row specified. +// The row parameter should be between 0 and t.Rows()-1. +func (t *RichText) rowLength(row int) int { + return len(t.row(row)) +} + +// rows returns the number of text rows in this text entry. +// The entry may be longer than required to show this amount of content. +func (t *RichText) rows() int { + if t.rowBounds == nil { // if the widget API is used before it is shown + t.updateRowBounds() + } + return len(t.rowBounds) +} + +// updateRowBounds updates the row bounds used to render properly the text widget. +// updateRowBounds should be invoked every time a segment Text, widget Wrapping or size changes. +func (t *RichText) updateRowBounds() { + th := t.Theme() + innerPadding := th.Size(theme.SizeNameInnerPadding) + fitSize := t.Size() + if t.scr != nil { + fitSize = t.scr.Content.MinSize() + } + fitSize.Height -= (innerPadding + t.inset.Height) * 2 + + var bounds []rowBoundary + maxWidth := t.Size().Width - 2*innerPadding + 2*t.inset.Width + wrapWidth := maxWidth + + var currentBound *rowBoundary + var iterateSegments func(segList []RichTextSegment) + iterateSegments = func(segList []RichTextSegment) { + for _, seg := range segList { + if parent, ok := seg.(RichTextBlock); ok { + segs := parent.Segments() + iterateSegments(segs) + if len(segs) > 0 && !segs[len(segs)-1].Inline() { + wrapWidth = maxWidth + currentBound = nil + } + continue + } + if _, ok := seg.(*TextSegment); !ok { + if currentBound == nil { + bound := rowBoundary{segments: []RichTextSegment{seg}} + bounds = append(bounds, bound) + currentBound = &bound + } else { + bounds[len(bounds)-1].segments = append(bounds[len(bounds)-1].segments, seg) + } + + itemMin := t.cachedSegmentVisual(seg, 0).MinSize() + if seg.Inline() { + wrapWidth -= itemMin.Width + } else { + wrapWidth = maxWidth + currentBound = nil + fitSize.Height -= itemMin.Height + th.Size(theme.SizeNameLineSpacing) + } + continue + } + textSeg := seg.(*TextSegment) + textStyle := textSeg.Style.TextStyle + textSize := textSeg.size() + + leftPad := float32(0) + if textSeg.Style == RichTextStyleBlockquote { + leftPad = innerPadding * 2 + } + retBounds, height := lineBounds(textSeg, t.Wrapping, t.Truncation, wrapWidth-leftPad, fyne.NewSize(maxWidth, fitSize.Height), func(text []rune) fyne.Size { + return fyne.MeasureText(string(text), textSize, textStyle) + }) + if currentBound != nil { + if len(retBounds) > 0 { + bounds[len(bounds)-1].end = retBounds[0].end // invalidate row ending as we have more content + bounds[len(bounds)-1].segments = append(bounds[len(bounds)-1].segments, seg) + bounds = append(bounds, retBounds[1:]...) + + fitSize.Height -= height + } + } else { + bounds = append(bounds, retBounds...) + + fitSize.Height -= height + } + currentBound = &bounds[len(bounds)-1] + if seg.Inline() { + last := bounds[len(bounds)-1] + begin := 0 + if len(last.segments) == 1 { + begin = last.begin + } + runes := []rune(textSeg.Text) + // check ranges - as we resize it can be wrong? + if begin > len(runes) { + begin = len(runes) + } + end := last.end + if end > len(runes) { + end = len(runes) + } + text := string(runes[begin:end]) + measured := fyne.MeasureText(text, textSeg.size(), textSeg.Style.TextStyle) + lastWidth := measured.Width + if len(retBounds) == 1 { + wrapWidth -= lastWidth + } else { + wrapWidth = maxWidth - lastWidth + } + } else { + currentBound = nil + wrapWidth = maxWidth + } + } + } + + iterateSegments(t.Segments) + t.rowBounds = bounds +} + +// RichTextBlock is an extension of a text segment that contains other segments +// +// Since: 2.1 +type RichTextBlock interface { + Segments() []RichTextSegment +} + +// Renderer +type textRenderer struct { + widget.BaseRenderer + obj *RichText +} + +func (r *textRenderer) Layout(size fyne.Size) { + th := r.obj.Theme() + bounds := r.obj.rowBounds + objs := r.Objects() + if r.obj.scr != nil { + r.obj.scr.Resize(size) + objs = r.obj.scr.Content.(*fyne.Container).Objects[1].(*fyne.Container).Objects + } + + // Accessing theme here is slow, so we cache the value + innerPadding := th.Size(theme.SizeNameInnerPadding) + lineSpacing := th.Size(theme.SizeNameLineSpacing) + + xInset := innerPadding - r.obj.inset.Width + left := xInset + yPos := innerPadding - r.obj.inset.Height + lineWidth := size.Width - left*2 + var rowItems []fyne.CanvasObject + rowAlign := fyne.TextAlignLeading + i := 0 + for row, bound := range bounds { + for segI := range bound.segments { + if i == len(objs) { + break // Refresh may not have created all objects for all rows yet... + } + inline := segI < len(bound.segments)-1 + obj := objs[i] + i++ + _, isText := obj.(*canvas.Text) + if !isText && !inline { + if len(rowItems) != 0 { + width, _ := r.layoutRow(rowItems, rowAlign, left, yPos, lineWidth) + left += width + rowItems = nil + } + height := obj.MinSize().Height + + obj.Move(fyne.NewPos(left, yPos)) + obj.Resize(fyne.NewSize(lineWidth, height)) + yPos += height + left = xInset + continue + } + rowItems = append(rowItems, obj) + if inline { + continue + } + + leftPad := float32(0) + if text, ok := bound.segments[0].(*TextSegment); ok { + rowAlign = text.Style.Alignment + if text.Style == RichTextStyleBlockquote { + leftPad = lineSpacing * 4 + } + } else if link, ok := bound.segments[0].(*HyperlinkSegment); ok { + rowAlign = link.Alignment + } + _, y := r.layoutRow(rowItems, rowAlign, left+leftPad, yPos, lineWidth-leftPad) + yPos += y + rowItems = nil + } + + lastSeg := bound.segments[len(bound.segments)-1] + if !lastSeg.Inline() && row < len(bounds)-1 && bounds[row+1].segments[0] != lastSeg { // ignore wrapped lines etc + yPos += lineSpacing + } + } +} + +// MinSize calculates the minimum size of a rich text widget. +// This is based on the contained text with a standard amount of padding added. +func (r *textRenderer) MinSize() fyne.Size { + th := r.obj.Theme() + textSize := th.Size(theme.SizeNameText) + innerPad := th.Size(theme.SizeNameInnerPadding) + + bounds := r.obj.rowBounds + wrap := r.obj.Wrapping + trunc := r.obj.Truncation + scroll := r.obj.Scroll + objs := r.Objects() + if r.obj.scr != nil { + objs = r.obj.scr.Content.(*fyne.Container).Objects[1].(*fyne.Container).Objects + } + + charMinSize := r.obj.charMinSize(false, fyne.TextStyle{}, textSize) + min := r.calculateMin(bounds, wrap, objs, charMinSize, th) + if r.obj.scr != nil { + r.obj.prop.SetMinSize(min) + } + + if trunc != fyne.TextTruncateOff && r.obj.Scroll == widget.ScrollNone { + minBounds := charMinSize + if wrap == fyne.TextWrapOff { + minBounds.Height = min.Height + } else { + minBounds = minBounds.Add(fyne.NewSquareSize(innerPad * 2).Subtract(r.obj.inset).Subtract(r.obj.inset)) + } + if trunc == fyne.TextTruncateClip { + return minBounds + } else if trunc == fyne.TextTruncateEllipsis { + ellipsisSize := fyne.MeasureText("…", th.Size(theme.SizeNameText), fyne.TextStyle{}) + return minBounds.AddWidthHeight(ellipsisSize.Width, 0) + } + } + + switch scroll { + case widget.ScrollBoth: + return fyne.NewSize(32, 32) + case widget.ScrollHorizontalOnly: + return fyne.NewSize(32, min.Height) + case widget.ScrollVerticalOnly: + return fyne.NewSize(min.Width, 32) + default: + return min + } +} + +func (r *textRenderer) calculateMin(bounds []rowBoundary, wrap fyne.TextWrap, objs []fyne.CanvasObject, + charMinSize fyne.Size, th fyne.Theme, +) fyne.Size { + height := float32(0) + width := float32(0) + rowHeight := float32(0) + rowWidth := float32(0) + trunc := r.obj.Truncation + innerPad := th.Size(theme.SizeNameInnerPadding) + + // Accessing the theme here is slow, so we cache the value + lineSpacing := th.Size(theme.SizeNameLineSpacing) + + i := 0 + for row, bound := range bounds { + for range bound.segments { + if i == len(objs) { + break // Refresh may not have created all objects for all rows yet... + } + obj := objs[i] + i++ + + min := obj.MinSize() + if img, ok := obj.(*richImage); ok { + if newMin := img.MinSize(); newMin != img.oldMin { + img.oldMin = newMin + + min := r.calculateMin(bounds, wrap, objs, charMinSize, th) + if r.obj.scr != nil { + r.obj.prop.SetMinSize(min) + } + r.Refresh() // TODO resolve this in a similar way to #2991 + } + } + rowHeight = fyne.Max(rowHeight, min.Height) + rowWidth += min.Width + } + + if wrap == fyne.TextWrapOff && trunc == fyne.TextTruncateOff { + width = fyne.Max(width, rowWidth) + } + height += rowHeight + rowHeight = 0 + rowWidth = 0 + + lastSeg := bound.segments[len(bound.segments)-1] + if !lastSeg.Inline() && row < len(bounds)-1 && bounds[row+1].segments[0] != lastSeg { // ignore wrapped lines etc + height += lineSpacing + } + } + + if height == 0 { + height = charMinSize.Height + } + return fyne.NewSize(width, height). + Add(fyne.NewSquareSize(innerPad * 2).Subtract(r.obj.inset).Subtract(r.obj.inset)) +} + +func (r *textRenderer) Refresh() { + bounds := r.obj.rowBounds + scroll := r.obj.Scroll + + var objs []fyne.CanvasObject + for _, bound := range bounds { + for i, seg := range bound.segments { + if _, ok := seg.(*TextSegment); !ok { + obj := r.obj.cachedSegmentVisual(seg, 0) + seg.Update(obj) + objs = append(objs, obj) + continue + } + + reuse := 0 + if i == 0 { + reuse = bound.firstSegmentReuse + } + obj := r.obj.cachedSegmentVisual(seg, reuse) + seg.Update(obj) + txt := obj.(*canvas.Text) + textSeg := seg.(*TextSegment) + runes := []rune(textSeg.Text) + + if i == 0 { + if len(bound.segments) == 1 { + txt.Text = string(runes[bound.begin:bound.end]) + } else { + txt.Text = string(runes[bound.begin:]) + } + } else if i == len(bound.segments)-1 && len(bound.segments) > 1 { + txt.Text = string(runes[:bound.end]) + } + if bound.ellipsis && i == len(bound.segments)-1 { + txt.Text = txt.Text + "…" + } + + if concealed(seg) { + txt.Text = strings.Repeat(passwordChar, len(runes)) + } + + objs = append(objs, txt) + } + } + + if r.obj.scr != nil { + r.obj.scr.Content = &fyne.Container{Layout: layout.NewStackLayout(), Objects: []fyne.CanvasObject{ + r.obj.prop, &fyne.Container{Objects: objs}, + }} + r.obj.scr.Direction = scroll + r.SetObjects([]fyne.CanvasObject{r.obj.scr}) + r.obj.scr.Refresh() + } else { + r.SetObjects(objs) + } + + r.Layout(r.obj.Size()) + canvas.Refresh(r.obj.super()) + + r.obj.cleanVisualCache() +} + +func (r *textRenderer) layoutRow(texts []fyne.CanvasObject, align fyne.TextAlign, xPos, yPos, lineWidth float32) (float32, float32) { + initialX := xPos + if len(texts) == 1 { + min := texts[0].MinSize() + if text, ok := texts[0].(*canvas.Text); ok { + texts[0].Resize(min) + xPad := float32(0) + switch text.Alignment { + case fyne.TextAlignLeading: + case fyne.TextAlignTrailing: + xPad = lineWidth - min.Width + case fyne.TextAlignCenter: + xPad = (lineWidth - min.Width) / 2 + } + texts[0].Move(fyne.NewPos(xPos+xPad, yPos)) + } else { + texts[0].Resize(fyne.NewSize(lineWidth, min.Height)) + texts[0].Move(fyne.NewPos(xPos, yPos)) + } + return min.Width, min.Height + } + height := float32(0) + tallestBaseline := float32(0) + realign := false + baselines := make([]float32, len(texts)) + + // Access to theme is slow, so we cache the text size + textSize := theme.SizeForWidget(theme.SizeNameText, r.obj) + + driver := fyne.CurrentApp().Driver() + for i, text := range texts { + var size fyne.Size + if txt, ok := text.(*canvas.Text); ok { + s, base := driver.RenderedTextSize(txt.Text, txt.TextSize, txt.TextStyle, txt.FontSource) + if base > tallestBaseline { + if tallestBaseline > 0 { + realign = true + } + tallestBaseline = base + } + size = s + baselines[i] = base + } else if c, ok := text.(*fyne.Container); ok { + wid := c.Objects[0] + if link, ok := wid.(*Hyperlink); ok { + s, base := driver.RenderedTextSize(link.Text, textSize, link.TextStyle, nil) + if base > tallestBaseline { + if tallestBaseline > 0 { + realign = true + } + tallestBaseline = base + } + size = s + baselines[i] = base + } + } + if size.IsZero() { + size = text.MinSize() + } + text.Resize(size) + text.Move(fyne.NewPos(xPos, yPos)) + + xPos += size.Width + if height == 0 { + height = size.Height + } else if height != size.Height { + height = fyne.Max(height, size.Height) + realign = true + } + } + + if realign { + for i, text := range texts { + delta := tallestBaseline - baselines[i] + text.Move(fyne.NewPos(text.Position().X, yPos+delta)) + } + } + + spare := lineWidth - xPos + switch align { + case fyne.TextAlignTrailing: + first := texts[0] + first.Resize(fyne.NewSize(first.Size().Width+spare, height)) + setAlign(first, fyne.TextAlignTrailing) + + for _, text := range texts[1:] { + text.Move(text.Position().Add(fyne.NewPos(spare, 0))) + } + case fyne.TextAlignCenter: + pad := spare / 2 + first := texts[0] + first.Resize(fyne.NewSize(first.Size().Width+pad, height)) + setAlign(first, fyne.TextAlignTrailing) + last := texts[len(texts)-1] + last.Resize(fyne.NewSize(last.Size().Width+pad, height)) + setAlign(last, fyne.TextAlignLeading) + + for _, text := range texts[1:] { + text.Move(text.Position().Add(fyne.NewPos(pad, 0))) + } + default: + last := texts[len(texts)-1] + last.Resize(fyne.NewSize(last.Size().Width+spare, height)) + setAlign(last, fyne.TextAlignLeading) + } + + return xPos - initialX, height +} + +// binarySearch accepts a function that checks if the text width less the maximum width and the start and end rune index +// binarySearch returns the index of rune located as close to the maximum line width as possible +func binarySearch(lessMaxWidth func(int, int) bool, low int, maxHigh int) int { + if low >= maxHigh { + return low + } + if lessMaxWidth(low, maxHigh) { + return maxHigh + } + high := low + delta := maxHigh - low + for delta > 0 { + delta /= 2 + if lessMaxWidth(low, high+delta) { + high += delta + } + } + for (high < maxHigh) && lessMaxWidth(low, high+1) { + high++ + } + return high +} + +// concealed returns true if the segment represents a password, meaning the text should be obscured. +func concealed(seg RichTextSegment) bool { + if text, ok := seg.(*TextSegment); ok { + return text.Style.concealed + } + + return false +} + +func ellipsisPriorBound(bounds []rowBoundary, trunc fyne.TextTruncation, width float32, measurer func([]rune) fyne.Size) []rowBoundary { + if trunc != fyne.TextTruncateEllipsis || len(bounds) == 0 { + return bounds + } + + prior := bounds[len(bounds)-1] + seg := prior.segments[0].(*TextSegment) + ellipsisSize := fyne.MeasureText("…", seg.size(), seg.Style.TextStyle) + + widthChecker := func(low int, high int) bool { + return measurer([]rune(seg.Text)[low:high]).Width <= width-ellipsisSize.Width + } + + limit := binarySearch(widthChecker, prior.begin, prior.end) + prior.end = limit + + prior.ellipsis = true + bounds[len(bounds)-1] = prior + return bounds +} + +// findSpaceIndex accepts a slice of runes and a fallback index +// findSpaceIndex returns the index of the last space in the text, or fallback if there are no spaces +func findSpaceIndex(text []rune, fallback int) int { + curIndex := fallback + for ; curIndex >= 0; curIndex-- { + if unicode.IsSpace(text[curIndex]) { + break + } + } + if curIndex < 0 { + return fallback + } + return curIndex +} + +func float32ToFixed266(f float32) fixed.Int26_6 { + return fixed.Int26_6(float64(f) * (1 << 6)) +} + +// lineBounds accepts a slice of Segments, a wrapping mode, a maximum size available to display and a function to +// measure text size. +// It will return a slice containing the boundary metadata of each line with the given wrapping applied and the +// total height required to render the boundaries at the given width/height constraints +func lineBounds(seg *TextSegment, wrap fyne.TextWrap, trunc fyne.TextTruncation, firstWidth float32, max fyne.Size, measurer func([]rune) fyne.Size) ([]rowBoundary, float32) { + lines := splitLines(seg) + + if wrap == fyne.TextWrap(fyne.TextTruncateClip) { + if trunc == fyne.TextTruncateOff { + trunc = fyne.TextTruncateClip + } + wrap = fyne.TextWrapOff + } + + if max.Width < 0 || wrap == fyne.TextWrapOff && trunc == fyne.TextTruncateOff { + return lines, 0 // don't bother returning a calculated height, our MinSize is going to cover it + } + + measureWidth := float32(math.Min(float64(firstWidth), float64(max.Width))) + text := []rune(seg.Text) + widthChecker := func(low int, high int) bool { + return measurer(text[low:high]).Width <= measureWidth + } + + reuse := 0 + yPos := float32(0) + var bounds []rowBoundary + for _, l := range lines { + low := l.begin + high := l.end + if low == high { + l.firstSegmentReuse = reuse + reuse++ + bounds = append(bounds, l) + continue + } + + switch wrap { + case fyne.TextWrapBreak: + for low < high { + measured := measurer(text[low:high]) + if yPos+measured.Height > max.Height && trunc != fyne.TextTruncateOff { + return ellipsisPriorBound(bounds, trunc, measureWidth, measurer), yPos + } + + if measured.Width <= measureWidth { + bounds = append(bounds, rowBoundary{[]RichTextSegment{seg}, reuse, low, high, false}) + reuse++ + low = high + high = l.end + measureWidth = max.Width + + yPos += measured.Height + } else { + newHigh := binarySearch(widthChecker, low, high) + if newHigh <= low { + bounds = append(bounds, rowBoundary{[]RichTextSegment{seg}, reuse, low, low + 1, false}) + reuse++ + low++ + + yPos += measured.Height + } else { + high = newHigh + } + } + } + case fyne.TextWrapWord: + for low < high { + sub := text[low:high] + measured := measurer(sub) + if yPos+measured.Height > max.Height && trunc != fyne.TextTruncateOff { + return ellipsisPriorBound(bounds, trunc, measureWidth, measurer), yPos + } + + subWidth := measured.Width + if subWidth <= measureWidth { + bounds = append(bounds, rowBoundary{[]RichTextSegment{seg}, reuse, low, high, false}) + reuse++ + low = high + high = l.end + if low < high && unicode.IsSpace(text[low]) { + low++ + } + measureWidth = max.Width + + yPos += measured.Height + } else { + oldHigh := high + last := low + len(sub) - 1 + fallback := binarySearch(widthChecker, low, last) - low + + if fallback < 1 { // even a character won't fit + include := 1 + ellipsis := false + if trunc == fyne.TextTruncateEllipsis { + include = 0 + ellipsis = true + } + bounds = append(bounds, rowBoundary{[]RichTextSegment{seg}, reuse, low, low + include, ellipsis}) + low++ + high = low + 1 + reuse++ + + yPos += measured.Height + if high > l.end { + return bounds, yPos + } + } else { + spaceIndex := findSpaceIndex(sub, fallback) + if spaceIndex == 0 { + spaceIndex = 1 + } + + high = low + spaceIndex + } + if high == fallback && subWidth <= max.Width { // add a newline as there is more space on next + bounds = append(bounds, rowBoundary{[]RichTextSegment{seg}, reuse, low, low, false}) + reuse++ + high = oldHigh + measureWidth = max.Width + + yPos += measured.Height + continue + } + } + } + default: + if trunc == fyne.TextTruncateEllipsis { + txt := []rune(seg.Text)[low:high] + end, full := truncateLimit(string(txt), seg.Visual().(*canvas.Text), int(measureWidth), []rune{'…'}) + high = low + end + bounds = append(bounds, rowBoundary{[]RichTextSegment{seg}, reuse, low, high, !full}) + reuse++ + } else if trunc == fyne.TextTruncateClip { + high = binarySearch(widthChecker, low, high) + bounds = append(bounds, rowBoundary{[]RichTextSegment{seg}, reuse, low, high, false}) + reuse++ + } + } + } + return bounds, yPos +} + +func setAlign(obj fyne.CanvasObject, align fyne.TextAlign) { + if text, ok := obj.(*canvas.Text); ok { + text.Alignment = align + return + } + if c, ok := obj.(*fyne.Container); ok { + wid := c.Objects[0] + if link, ok := wid.(*Hyperlink); ok { + link.Alignment = align + link.Refresh() + } + } +} + +// splitLines accepts a text segment and returns a slice of boundary metadata denoting the +// start and end indices of each line delimited by the newline character. +func splitLines(seg *TextSegment) []rowBoundary { + var low, high int + var lines []rowBoundary + text := []rune(seg.Text) + length := len(text) + for i := 0; i < length; i++ { + if text[i] == '\n' { + high = i + lines = append(lines, rowBoundary{[]RichTextSegment{seg}, len(lines), low, high, false}) + low = i + 1 + } + } + return append(lines, rowBoundary{[]RichTextSegment{seg}, len(lines), low, length, false}) +} + +func truncateLimit(s string, text *canvas.Text, limit int, ellipsis []rune) (int, bool) { + face := paint.CachedFontFace(text.TextStyle, text.FontSource, text) + + runes := []rune(s) + in := shaping.Input{ + Text: ellipsis, + RunStart: 0, + RunEnd: len(ellipsis), + Direction: di.DirectionLTR, + Face: face.Fonts.ResolveFace(ellipsis[0]), + Size: float32ToFixed266(text.TextSize), + } + shaper := &shaping.HarfbuzzShaper{} + segmenter := &shaping.Segmenter{} + + conf := shaping.WrapConfig{} + conf = conf.WithTruncator(shaper, in) + conf.BreakPolicy = shaping.WhenNecessary + conf.TruncateAfterLines = 1 + l := shaping.LineWrapper{} + + in.Text = runes + in.RunEnd = len(runes) + ins := segmenter.Split(in, face.Fonts) + outs := make([]shaping.Output, len(ins)) + for i, in := range ins { + outs[i] = shaper.Shape(in) + } + + l.Prepare(conf, runes, shaping.NewSliceIterator(outs)) + wrapped, done := l.WrapNextLine(limit) + + count := len(runes) + if wrapped.Truncated != 0 { + count -= wrapped.Truncated + count += len(ellipsis) + } + + full := done && count == len(runes) + if !full && len(ellipsis) > 0 { + count-- + } + return count, full +} + +type rowBoundary struct { + segments []RichTextSegment + firstSegmentReuse int + begin, end int + ellipsis bool +} diff --git a/vendor/fyne.io/fyne/v2/widget/richtext_objects.go b/vendor/fyne.io/fyne/v2/widget/richtext_objects.go new file mode 100644 index 0000000..a79eb05 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/richtext_objects.go @@ -0,0 +1,567 @@ +package widget + +import ( + "image/color" + "net/url" + "strconv" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/internal/scale" + "fyne.io/fyne/v2/theme" +) + +var ( + // RichTextStyleBlockquote represents a quote presented in an indented block. + // + // Since: 2.1 + RichTextStyleBlockquote = RichTextStyle{ + ColorName: theme.ColorNameForeground, + Inline: false, + SizeName: theme.SizeNameText, + TextStyle: fyne.TextStyle{Italic: true}, + } + // RichTextStyleCodeBlock represents a code blog segment. + // + // Since: 2.1 + RichTextStyleCodeBlock = RichTextStyle{ + ColorName: theme.ColorNameForeground, + Inline: false, + SizeName: theme.SizeNameText, + TextStyle: fyne.TextStyle{Monospace: true}, + } + // RichTextStyleCodeInline represents an inline code segment. + // + // Since: 2.1 + RichTextStyleCodeInline = RichTextStyle{ + ColorName: theme.ColorNameForeground, + Inline: true, + SizeName: theme.SizeNameText, + TextStyle: fyne.TextStyle{Monospace: true}, + } + // RichTextStyleEmphasis represents regular text with emphasis. + // + // Since: 2.1 + RichTextStyleEmphasis = RichTextStyle{ + ColorName: theme.ColorNameForeground, + Inline: true, + SizeName: theme.SizeNameText, + TextStyle: fyne.TextStyle{Italic: true}, + } + // RichTextStyleHeading represents a heading text that stands on its own line. + // + // Since: 2.1 + RichTextStyleHeading = RichTextStyle{ + ColorName: theme.ColorNameForeground, + Inline: false, + SizeName: theme.SizeNameHeadingText, + TextStyle: fyne.TextStyle{Bold: true}, + } + // RichTextStyleInline represents standard text that can be surrounded by other elements. + // + // Since: 2.1 + RichTextStyleInline = RichTextStyle{ + ColorName: theme.ColorNameForeground, + Inline: true, + SizeName: theme.SizeNameText, + } + // RichTextStyleParagraph represents standard text that should appear separate from other text. + // + // Since: 2.1 + RichTextStyleParagraph = RichTextStyle{ + ColorName: theme.ColorNameForeground, + Inline: false, + SizeName: theme.SizeNameText, + } + // RichTextStylePassword represents standard sized text where the characters are obscured. + // + // Since: 2.1 + RichTextStylePassword = RichTextStyle{ + ColorName: theme.ColorNameForeground, + Inline: true, + SizeName: theme.SizeNameText, + concealed: true, + } + // RichTextStyleStrong represents regular text with a strong emphasis. + // + // Since: 2.1 + RichTextStyleStrong = RichTextStyle{ + ColorName: theme.ColorNameForeground, + Inline: true, + SizeName: theme.SizeNameText, + TextStyle: fyne.TextStyle{Bold: true}, + } + // RichTextStyleSubHeading represents a sub-heading text that stands on its own line. + // + // Since: 2.1 + RichTextStyleSubHeading = RichTextStyle{ + ColorName: theme.ColorNameForeground, + Inline: false, + SizeName: theme.SizeNameSubHeadingText, + TextStyle: fyne.TextStyle{Bold: true}, + } +) + +// HyperlinkSegment represents a hyperlink within a rich text widget. +// +// Since: 2.1 +type HyperlinkSegment struct { + Alignment fyne.TextAlign + Text string + URL *url.URL + + // OnTapped overrides the default `fyne.OpenURL` call when the link is tapped + // + // Since: 2.4 + OnTapped func() `json:"-"` +} + +// Inline returns true as hyperlinks are inside other elements. +func (h *HyperlinkSegment) Inline() bool { + return true +} + +// Textual returns the content of this segment rendered to plain text. +func (h *HyperlinkSegment) Textual() string { + return h.Text +} + +// Visual returns a new instance of a hyperlink widget required to render this segment. +func (h *HyperlinkSegment) Visual() fyne.CanvasObject { + link := NewHyperlink(h.Text, h.URL) + link.Alignment = h.Alignment + link.OnTapped = h.OnTapped + return &fyne.Container{Layout: &unpadTextWidgetLayout{parent: link}, Objects: []fyne.CanvasObject{link}} +} + +// Update applies the current state of this hyperlink segment to an existing visual. +func (h *HyperlinkSegment) Update(o fyne.CanvasObject) { + link := o.(*fyne.Container).Objects[0].(*Hyperlink) + link.Text = h.Text + link.URL = h.URL + link.Alignment = h.Alignment + link.OnTapped = h.OnTapped + link.Refresh() +} + +// Select tells the segment that the user is selecting the content between the two positions. +func (h *HyperlinkSegment) Select(begin, end fyne.Position) { + // no-op: this will be added when we progress to editor +} + +// SelectedText should return the text representation of any content currently selected through the Select call. +func (h *HyperlinkSegment) SelectedText() string { + // no-op: this will be added when we progress to editor + return "" +} + +// Unselect tells the segment that the user is has cancelled the previous selection. +func (h *HyperlinkSegment) Unselect() { + // no-op: this will be added when we progress to editor +} + +// ImageSegment represents an image within a rich text widget. +// +// Since: 2.3 +type ImageSegment struct { + Source fyne.URI + Title string + + // Alignment specifies the horizontal alignment of this image segment + // Since: 2.4 + Alignment fyne.TextAlign +} + +// Inline returns false as images in rich text are blocks. +func (i *ImageSegment) Inline() bool { + return false +} + +// Textual returns the content of this segment rendered to plain text. +func (i *ImageSegment) Textual() string { + return "Image " + i.Title +} + +// Visual returns a new instance of an image widget required to render this segment. +func (i *ImageSegment) Visual() fyne.CanvasObject { + return newRichImage(i.Source, i.Alignment) +} + +// Update applies the current state of this image segment to an existing visual. +func (i *ImageSegment) Update(o fyne.CanvasObject) { + newer := canvas.NewImageFromURI(i.Source) + img := o.(*richImage) + + // one of the following will be used + img.img.File = newer.File + img.img.Resource = newer.Resource + img.setAlign(i.Alignment) + + img.Refresh() +} + +// Select tells the segment that the user is selecting the content between the two positions. +func (i *ImageSegment) Select(begin, end fyne.Position) { + // no-op: this will be added when we progress to editor +} + +// SelectedText should return the text representation of any content currently selected through the Select call. +func (i *ImageSegment) SelectedText() string { + // no-op: images have no text rendering + return "" +} + +// Unselect tells the segment that the user is has cancelled the previous selection. +func (i *ImageSegment) Unselect() { + // no-op: this will be added when we progress to editor +} + +// ListSegment includes an itemised list with the content set using the Items field. +// +// Since: 2.1 +type ListSegment struct { + Items []RichTextSegment + Ordered bool + + // startIndex is the starting number - 1 (If it is ordered). Unordered lists + // ignore startIndex. + // + // startIndex is set to start - 1 to allow the empty value of ListSegment to have a starting + // number of 1, while also allowing the caller to override the starting + // number to any int, including 0. + startIndex int +} + +// SetStartNumber sets the starting number for an ordered list. +// Unordered lists are not affected. +// +// Since: 2.7 +func (l *ListSegment) SetStartNumber(s int) { + l.startIndex = s - 1 +} + +// StartNumber return the starting number for an ordered list. +// +// Since: 2.7 +func (l *ListSegment) StartNumber() int { + return l.startIndex + 1 +} + +// Inline returns false as a list should be in a block. +func (l *ListSegment) Inline() bool { + return false +} + +// Segments returns the segments required to draw bullets before each item +func (l *ListSegment) Segments() []RichTextSegment { + out := make([]RichTextSegment, len(l.Items)) + for i, in := range l.Items { + txt := "• " + if l.Ordered { + txt = strconv.Itoa(i+l.startIndex+1) + "." + } + bullet := &TextSegment{Text: txt + " ", Style: RichTextStyleStrong} + out[i] = &ParagraphSegment{Texts: []RichTextSegment{ + bullet, + in, + }} + } + return out +} + +// Textual returns no content for a list as the content is in sub-segments. +func (l *ListSegment) Textual() string { + return "" +} + +// Visual returns no additional elements for this segment. +func (l *ListSegment) Visual() fyne.CanvasObject { + return nil +} + +// Update doesn't need to change a list visual. +func (l *ListSegment) Update(fyne.CanvasObject) { +} + +// Select does nothing for a list container. +func (l *ListSegment) Select(_, _ fyne.Position) { +} + +// SelectedText returns the empty string for this list. +func (l *ListSegment) SelectedText() string { + return "" +} + +// Unselect does nothing for a list container. +func (l *ListSegment) Unselect() { +} + +// ParagraphSegment wraps a number of text elements in a paragraph. +// It is similar to using a list of text elements when the final style is RichTextStyleParagraph. +// +// Since: 2.1 +type ParagraphSegment struct { + Texts []RichTextSegment +} + +// Inline returns false as a paragraph should be in a block. +func (p *ParagraphSegment) Inline() bool { + return false +} + +// Segments returns the list of text elements in this paragraph. +func (p *ParagraphSegment) Segments() []RichTextSegment { + return p.Texts +} + +// Textual returns no content for a paragraph container. +func (p *ParagraphSegment) Textual() string { + return "" +} + +// Visual returns the no extra elements. +func (p *ParagraphSegment) Visual() fyne.CanvasObject { + return nil +} + +// Update doesn't need to change a paragraph container. +func (p *ParagraphSegment) Update(fyne.CanvasObject) { +} + +// Select does nothing for a paragraph container. +func (p *ParagraphSegment) Select(_, _ fyne.Position) { +} + +// SelectedText returns the empty string for this paragraph container. +func (p *ParagraphSegment) SelectedText() string { + return "" +} + +// Unselect does nothing for a paragraph container. +func (p *ParagraphSegment) Unselect() { +} + +// SeparatorSegment includes a horizontal separator in a rich text widget. +// +// Since: 2.1 +type SeparatorSegment struct { + _ bool // Without this a pointer to SeparatorSegment will always be the same. +} + +// Inline returns false as a separator should be full width. +func (s *SeparatorSegment) Inline() bool { + return false +} + +// Textual returns no content for a separator element. +func (s *SeparatorSegment) Textual() string { + return "" +} + +// Visual returns a new instance of a separator widget for this segment. +func (s *SeparatorSegment) Visual() fyne.CanvasObject { + return NewSeparator() +} + +// Update doesn't need to change a separator visual. +func (s *SeparatorSegment) Update(fyne.CanvasObject) { +} + +// Select does nothing for a separator. +func (s *SeparatorSegment) Select(_, _ fyne.Position) { +} + +// SelectedText returns the empty string for this separator. +func (s *SeparatorSegment) SelectedText() string { + return "" // TODO maybe return "---\n"? +} + +// Unselect does nothing for a separator. +func (s *SeparatorSegment) Unselect() { +} + +// RichTextStyle describes the details of a text object inside a RichText widget. +// +// Since: 2.1 +type RichTextStyle struct { + Alignment fyne.TextAlign + ColorName fyne.ThemeColorName + Inline bool + SizeName fyne.ThemeSizeName // The theme name of the text size to use, if blank will be the standard text size + TextStyle fyne.TextStyle + + // an internal detail where we obscure password fields + concealed bool +} + +// RichTextSegment describes any element that can be rendered in a RichText widget. +// +// Since: 2.1 +type RichTextSegment interface { + Inline() bool + Textual() string + Update(fyne.CanvasObject) + Visual() fyne.CanvasObject + + Select(pos1, pos2 fyne.Position) + SelectedText() string + Unselect() +} + +// TextSegment represents the styling for a segment of rich text. +// +// Since: 2.1 +type TextSegment struct { + Style RichTextStyle + Text string + + parent *RichText +} + +// Inline should return true if this text can be included within other elements, or false if it creates a new block. +func (t *TextSegment) Inline() bool { + return t.Style.Inline +} + +// Textual returns the content of this segment rendered to plain text. +func (t *TextSegment) Textual() string { + return t.Text +} + +// Visual returns a new instance of a graphical element required to render this segment. +func (t *TextSegment) Visual() fyne.CanvasObject { + obj := canvas.NewText(t.Text, t.color()) + + t.Update(obj) + return obj +} + +// Update applies the current state of this text segment to an existing visual. +func (t *TextSegment) Update(o fyne.CanvasObject) { + obj := o.(*canvas.Text) + obj.Text = t.Text + obj.Color = t.color() + obj.Alignment = t.Style.Alignment + obj.TextStyle = t.Style.TextStyle + obj.TextSize = t.size() + obj.Refresh() +} + +// Select tells the segment that the user is selecting the content between the two positions. +func (t *TextSegment) Select(begin, end fyne.Position) { + // no-op: this will be added when we progress to editor +} + +// SelectedText should return the text representation of any content currently selected through the Select call. +func (t *TextSegment) SelectedText() string { + // no-op: this will be added when we progress to editor + return "" +} + +// Unselect tells the segment that the user is has cancelled the previous selection. +func (t *TextSegment) Unselect() { + // no-op: this will be added when we progress to editor +} + +func (t *TextSegment) color() color.Color { + if t.Style.ColorName != "" { + return theme.ColorForWidget(t.Style.ColorName, t.parent) + } + + return theme.ColorForWidget(theme.ColorNameForeground, t.parent) +} + +func (t *TextSegment) size() float32 { + if t.Style.SizeName != "" { + i := theme.SizeForWidget(t.Style.SizeName, t.parent) + return i + } + + i := theme.SizeForWidget(theme.SizeNameText, t.parent) + return i +} + +type richImage struct { + BaseWidget + align fyne.TextAlign + img *canvas.Image + oldMin fyne.Size + layout *fyne.Container + min fyne.Size +} + +func newRichImage(u fyne.URI, align fyne.TextAlign) *richImage { + img := canvas.NewImageFromURI(u) + img.FillMode = canvas.ImageFillOriginal + i := &richImage{img: img, align: align} + i.ExtendBaseWidget(i) + return i +} + +func (r *richImage) CreateRenderer() fyne.WidgetRenderer { + r.layout = &fyne.Container{Layout: &richImageLayout{r}, Objects: []fyne.CanvasObject{r.img}} + return NewSimpleRenderer(r.layout) +} + +func (r *richImage) MinSize() fyne.Size { + orig := r.img.MinSize() + c := fyne.CurrentApp().Driver().CanvasForObject(r) + if c == nil { + return r.oldMin // not yet rendered + } + + // unscale the image so it is not varying based on canvas + w := scale.ToScreenCoordinate(c, orig.Width) + h := scale.ToScreenCoordinate(c, orig.Height) + // we return size / 2 as this assumes a HiDPI / 2x image scaling + r.min = fyne.NewSize(float32(w)/2, float32(h)/2) + return r.min +} + +func (r *richImage) setAlign(a fyne.TextAlign) { + if r.layout != nil { + r.layout.Refresh() + } + r.align = a +} + +type richImageLayout struct { + r *richImage +} + +func (r *richImageLayout) Layout(_ []fyne.CanvasObject, s fyne.Size) { + r.r.img.Resize(r.r.min) + gap := float32(0) + + switch r.r.align { + case fyne.TextAlignCenter: + gap = (s.Width - r.r.min.Width) / 2 + case fyne.TextAlignTrailing: + gap = s.Width - r.r.min.Width + } + + r.r.img.Move(fyne.NewPos(gap, 0)) +} + +func (r *richImageLayout) MinSize(_ []fyne.CanvasObject) fyne.Size { + return r.r.min +} + +type unpadTextWidgetLayout struct { + parent fyne.Widget +} + +func (u *unpadTextWidgetLayout) Layout(o []fyne.CanvasObject, s fyne.Size) { + innerPad := theme.SizeForWidget(theme.SizeNameInnerPadding, u.parent) + pad := innerPad * -1 + pad2 := pad * -2 + + o[0].Move(fyne.NewPos(pad, pad)) + o[0].Resize(s.Add(fyne.NewSize(pad2, pad2))) +} + +func (u *unpadTextWidgetLayout) MinSize(o []fyne.CanvasObject) fyne.Size { + innerPad := theme.SizeForWidget(theme.SizeNameInnerPadding, u.parent) + pad := innerPad * 2 + return o[0].MinSize().Subtract(fyne.NewSize(pad, pad)) +} diff --git a/vendor/fyne.io/fyne/v2/widget/select.go b/vendor/fyne.io/fyne/v2/widget/select.go new file mode 100644 index 0000000..8cf40e7 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/select.go @@ -0,0 +1,461 @@ +package widget + +import ( + "fmt" + "image/color" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/data/binding" + "fyne.io/fyne/v2/driver/desktop" + "fyne.io/fyne/v2/theme" +) + +const defaultPlaceHolder string = "(Select one)" + +var ( + _ fyne.Widget = (*Select)(nil) + _ desktop.Hoverable = (*Select)(nil) + _ fyne.Tappable = (*Select)(nil) + _ fyne.Focusable = (*Select)(nil) + _ fyne.Disableable = (*Select)(nil) +) + +// Select widget has a list of options, with the current one shown, and triggers an event func when clicked +type Select struct { + DisableableWidget + + // Alignment sets the text alignment of the select and its list of options. + // + // Since: 2.1 + Alignment fyne.TextAlign + Selected string + Options []string + PlaceHolder string + OnChanged func(string) `json:"-"` + + binder basicBinder + + focused bool + hovered bool + popUp *PopUpMenu + tapAnim *fyne.Animation +} + +// NewSelect creates a new select widget with the set list of options and changes handler +func NewSelect(options []string, changed func(string)) *Select { + s := &Select{ + OnChanged: changed, + Options: options, + PlaceHolder: defaultPlaceHolder, + } + s.ExtendBaseWidget(s) + return s +} + +// NewSelectWithData returns a new select widget connected to the specified data source. +// +// Since: 2.6 +func NewSelectWithData(options []string, data binding.String) *Select { + sel := NewSelect(options, nil) + sel.Bind(data) + + return sel +} + +// Bind connects the specified data source to this select. +// The current value will be displayed and any changes in the data will cause the widget +// to update. +// +// Since: 2.6 +func (s *Select) Bind(data binding.String) { + s.binder.SetCallback(s.updateFromData) + s.binder.Bind(data) + + s.OnChanged = func(_ string) { + s.binder.CallWithData(s.writeData) + } +} + +// ClearSelected clears the current option of the select widget. After +// clearing the current option, the Select widget's PlaceHolder will +// be displayed. +func (s *Select) ClearSelected() { + s.updateSelected("") +} + +// CreateRenderer is a private method to Fyne which links this widget to its renderer +func (s *Select) CreateRenderer() fyne.WidgetRenderer { + s.ExtendBaseWidget(s) + th := s.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + icon := NewIcon(th.Icon(theme.IconNameArrowDropDown)) + if s.PlaceHolder == "" { + s.PlaceHolder = defaultPlaceHolder + } + txtProv := NewRichTextWithText(s.Selected) + txtProv.inset = fyne.NewSquareSize(th.Size(theme.SizeNamePadding)) + txtProv.ExtendBaseWidget(txtProv) + txtProv.Truncation = fyne.TextTruncateEllipsis + if s.Disabled() { + txtProv.Segments[0].(*TextSegment).Style.ColorName = theme.ColorNameDisabled + } + + background := &canvas.Rectangle{} + tapBG := canvas.NewRectangle(color.Transparent) + s.tapAnim = newButtonTapAnimation(tapBG, s, th) + s.tapAnim.Curve = fyne.AnimationEaseOut + objects := []fyne.CanvasObject{background, tapBG, txtProv, icon} + r := &selectRenderer{icon, txtProv, background, objects, s} + background.FillColor = r.bgColor(th, v) + background.CornerRadius = th.Size(theme.SizeNameInputRadius) + r.updateIcon(th) + r.updateLabel() + return r +} + +// FocusGained is called after this Select has gained focus. +func (s *Select) FocusGained() { + s.focused = true + s.Refresh() +} + +// FocusLost is called after this Select has lost focus. +func (s *Select) FocusLost() { + s.focused = false + s.Refresh() +} + +// Hide hides the select. +func (s *Select) Hide() { + if s.popUp != nil { + s.popUp.Hide() + s.popUp = nil + } + s.BaseWidget.Hide() +} + +// MinSize returns the size that this widget should not shrink below +func (s *Select) MinSize() fyne.Size { + s.ExtendBaseWidget(s) + return s.BaseWidget.MinSize() +} + +// MouseIn is called when a desktop pointer enters the widget +func (s *Select) MouseIn(*desktop.MouseEvent) { + s.hovered = true + s.Refresh() +} + +// MouseMoved is called when a desktop pointer hovers over the widget +func (s *Select) MouseMoved(*desktop.MouseEvent) { +} + +// MouseOut is called when a desktop pointer exits the widget +func (s *Select) MouseOut() { + s.hovered = false + s.Refresh() +} + +// Move changes the relative position of the select. +func (s *Select) Move(pos fyne.Position) { + s.BaseWidget.Move(pos) + + if s.popUp != nil { + s.popUp.Move(s.popUpPos()) + } +} + +// Resize sets a new size for a widget. +// Note this should not be used if the widget is being managed by a Layout within a Container. +func (s *Select) Resize(size fyne.Size) { + s.BaseWidget.Resize(size) + + if s.popUp != nil { + s.popUp.Resize(fyne.NewSize(size.Width, s.popUp.MinSize().Height)) + } +} + +// SelectedIndex returns the index value of the currently selected item in Options list. +// It will return -1 if there is no selection. +func (s *Select) SelectedIndex() int { + for i, option := range s.Options { + if s.Selected == option { + return i + } + } + return -1 // not selected/found +} + +// SetOptions updates the list of options available and refreshes the widget +// +// Since: 2.4 +func (s *Select) SetOptions(options []string) { + s.Options = options + s.Refresh() +} + +// SetSelected sets the current option of the select widget +func (s *Select) SetSelected(text string) { + for _, option := range s.Options { + if text == option { + s.updateSelected(text) + } + } +} + +// SetSelectedIndex will set the Selected option from the value in Options list at index position. +func (s *Select) SetSelectedIndex(index int) { + if index < 0 || index >= len(s.Options) { + return + } + + s.updateSelected(s.Options[index]) +} + +// Tapped is called when a pointer tapped event is captured and triggers any tap handler +func (s *Select) Tapped(*fyne.PointEvent) { + if s.Disabled() { + return + } + + if !s.focused { + focusIfNotMobile(s.super()) + } + + s.tapAnimation() + s.Refresh() + + s.showPopUp() +} + +// TypedKey is called if a key event happens while this Select is focused. +func (s *Select) TypedKey(event *fyne.KeyEvent) { + switch event.Name { + case fyne.KeySpace, fyne.KeyUp, fyne.KeyDown: + s.showPopUp() + case fyne.KeyRight: + i := s.SelectedIndex() + 1 + if i >= len(s.Options) { + i = 0 + } + s.SetSelectedIndex(i) + case fyne.KeyLeft: + i := s.SelectedIndex() - 1 + if i < 0 { + i = len(s.Options) - 1 + } + s.SetSelectedIndex(i) + } +} + +// TypedRune is called if a text event happens while this Select is focused. +func (s *Select) TypedRune(_ rune) { + // intentionally left blank +} + +// Unbind disconnects any configured data source from this Select. +// The current value will remain at the last value of the data source. +// +// Since: 2.6 +func (s *Select) Unbind() { + s.OnChanged = nil + s.binder.Unbind() +} + +func (s *Select) popUpPos() fyne.Position { + buttonPos := fyne.CurrentApp().Driver().AbsolutePositionForObject(s.super()) + return buttonPos.Add(fyne.NewPos(0, s.Size().Height-s.Theme().Size(theme.SizeNameInputBorder))) +} + +func (s *Select) showPopUp() { + items := make([]*fyne.MenuItem, len(s.Options)) + for i := range s.Options { + text := s.Options[i] // capture + items[i] = fyne.NewMenuItem(text, func() { + s.updateSelected(text) + s.popUp = nil + }) + } + + c := fyne.CurrentApp().Driver().CanvasForObject(s.super()) + pop := NewPopUpMenu(fyne.NewMenu("", items...), c) + pop.alignment = s.Alignment + pop.ShowAtPosition(s.popUpPos()) + pop.Resize(fyne.NewSize(s.Size().Width, pop.MinSize().Height)) + pop.OnDismiss = func() { + pop.Hide() + if s.popUp == pop { + s.popUp = nil + } + } + s.popUp = pop +} + +func (s *Select) tapAnimation() { + if s.tapAnim == nil { + return + } + s.tapAnim.Stop() + + if fyne.CurrentApp().Settings().ShowAnimations() { + s.tapAnim.Start() + } +} + +func (s *Select) updateFromData(data binding.DataItem) { + if data == nil { + return + } + stringSource, ok := data.(binding.String) + if !ok { + return + } + + val, err := stringSource.Get() + if err != nil { + return + } + s.SetSelected(val) +} + +func (s *Select) updateSelected(text string) { + s.Selected = text + + if s.OnChanged != nil { + s.OnChanged(s.Selected) + } + + s.Refresh() +} + +func (s *Select) writeData(data binding.DataItem) { + if data == nil { + return + } + stringTarget, ok := data.(binding.String) + if !ok { + return + } + currentValue, err := stringTarget.Get() + if err != nil { + return + } + if currentValue != s.Selected { + err := stringTarget.Set(s.Selected) + if err != nil { + fyne.LogError(fmt.Sprintf("Failed to set binding value to %s", s.Selected), err) + } + } +} + +type selectRenderer struct { + icon *Icon + label *RichText + background *canvas.Rectangle + + objects []fyne.CanvasObject + combo *Select +} + +func (s *selectRenderer) Objects() []fyne.CanvasObject { + return s.objects +} + +func (s *selectRenderer) Destroy() {} + +// Layout the components of the button widget +func (s *selectRenderer) Layout(size fyne.Size) { + th := s.combo.Theme() + pad := th.Size(theme.SizeNamePadding) + iconSize := th.Size(theme.SizeNameInlineIcon) + innerPad := th.Size(theme.SizeNameInnerPadding) + s.background.Resize(fyne.NewSize(size.Width, size.Height)) + s.label.inset = fyne.NewSquareSize(pad) + + iconPos := fyne.NewPos(size.Width-iconSize-innerPad, (size.Height-iconSize)/2) + labelSize := fyne.NewSize(iconPos.X-pad, s.label.MinSize().Height) + + s.label.Resize(labelSize) + s.label.Move(fyne.NewPos(pad, (size.Height-labelSize.Height)/2)) + + s.icon.Resize(fyne.NewSquareSize(iconSize)) + s.icon.Move(iconPos) +} + +// MinSize calculates the minimum size of a select button. +// This is based on the selected text, the drop icon and a standard amount of padding added. +func (s *selectRenderer) MinSize() fyne.Size { + th := s.combo.Theme() + innerPad := th.Size(theme.SizeNameInnerPadding) + + minPlaceholderWidth := fyne.MeasureText(s.combo.PlaceHolder, th.Size(theme.SizeNameText), fyne.TextStyle{}).Width + min := s.label.MinSize() + min.Width = minPlaceholderWidth + min = min.Add(fyne.NewSize(innerPad*3, innerPad)) + return min.Add(fyne.NewSize(th.Size(theme.SizeNameInlineIcon)+innerPad, 0)) +} + +func (s *selectRenderer) Refresh() { + th := s.combo.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + s.updateLabel() + s.updateIcon(th) + s.background.FillColor = s.bgColor(th, v) + s.background.CornerRadius = s.combo.Theme().Size(theme.SizeNameInputRadius) + + s.Layout(s.combo.Size()) + if s.combo.popUp != nil { + s.combo.popUp.alignment = s.combo.Alignment + s.combo.popUp.Move(s.combo.popUpPos()) + s.combo.popUp.Resize(fyne.NewSize(s.combo.Size().Width, s.combo.popUp.MinSize().Height)) + s.combo.popUp.Refresh() + } + s.background.Refresh() + canvas.Refresh(s.combo.super()) +} + +func (s *selectRenderer) bgColor(th fyne.Theme, v fyne.ThemeVariant) color.Color { + if s.combo.Disabled() { + return th.Color(theme.ColorNameDisabledButton, v) + } + if s.combo.focused { + return th.Color(theme.ColorNameFocus, v) + } + if s.combo.hovered { + return th.Color(theme.ColorNameHover, v) + } + return th.Color(theme.ColorNameInputBackground, v) +} + +func (s *selectRenderer) updateIcon(th fyne.Theme) { + icon := th.Icon(theme.IconNameArrowDropDown) + if s.combo.Disabled() { + s.icon.Resource = theme.NewDisabledResource(icon) + } else { + s.icon.Resource = icon + } + s.icon.Refresh() +} + +func (s *selectRenderer) updateLabel() { + if s.combo.PlaceHolder == "" { + s.combo.PlaceHolder = defaultPlaceHolder + } + + segment := s.label.Segments[0].(*TextSegment) + segment.Style.Alignment = s.combo.Alignment + if s.combo.Disabled() { + segment.Style.ColorName = theme.ColorNameDisabled + } else { + segment.Style.ColorName = theme.ColorNameForeground + } + if s.combo.Selected == "" { + segment.Text = s.combo.PlaceHolder + } else { + segment.Text = s.combo.Selected + } + s.label.Refresh() +} diff --git a/vendor/fyne.io/fyne/v2/widget/select_entry.go b/vendor/fyne.io/fyne/v2/widget/select_entry.go new file mode 100644 index 0000000..eaff8f7 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/select_entry.go @@ -0,0 +1,108 @@ +package widget + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/theme" +) + +var ( + _ fyne.Widget = (*SelectEntry)(nil) + _ fyne.Disableable = (*SelectEntry)(nil) +) + +// SelectEntry is an input field which supports selecting from a fixed set of options. +type SelectEntry struct { + Entry + dropDown *fyne.Menu + popUp *PopUpMenu + options []string +} + +// NewSelectEntry creates a SelectEntry. +func NewSelectEntry(options []string) *SelectEntry { + e := &SelectEntry{options: options} + e.ExtendBaseWidget(e) + e.Wrapping = fyne.TextWrap(fyne.TextTruncateClip) + return e +} + +// CreateRenderer returns a new renderer for this select entry. +func (e *SelectEntry) CreateRenderer() fyne.WidgetRenderer { + e.ExtendBaseWidget(e) + e.SetOptions(e.options) + return e.Entry.CreateRenderer() +} + +// Enable this widget, updating any style or features appropriately. +func (e *SelectEntry) Enable() { + if e.ActionItem != nil { + e.ActionItem.(fyne.Disableable).Enable() + } + e.Entry.Enable() +} + +// Disable this widget so that it cannot be interacted with, updating any style appropriately. +func (e *SelectEntry) Disable() { + if e.ActionItem != nil { + e.ActionItem.(fyne.Disableable).Disable() + } + e.Entry.Disable() +} + +// MinSize returns the minimal size of the select entry. +func (e *SelectEntry) MinSize() fyne.Size { + e.ExtendBaseWidget(e) + return e.Entry.MinSize() +} + +// Move changes the relative position of the select entry. +func (e *SelectEntry) Move(pos fyne.Position) { + e.Entry.Move(pos) + if e.popUp != nil { + e.popUp.Move(e.popUpPos()) + } +} + +// Resize changes the size of the select entry. +func (e *SelectEntry) Resize(size fyne.Size) { + e.Entry.Resize(size) + if e.popUp != nil { + e.popUp.Resize(fyne.NewSize(size.Width, e.popUp.Size().Height)) + } +} + +// SetOptions sets the options the user might select from. +func (e *SelectEntry) SetOptions(options []string) { + e.options = options + items := make([]*fyne.MenuItem, len(options)) + for i, option := range options { + option := option // capture + items[i] = fyne.NewMenuItem(option, func() { e.SetText(option) }) + } + e.dropDown = fyne.NewMenu("", items...) + + if e.ActionItem == nil { + e.ActionItem = e.setupDropDown() + if e.Disabled() { + e.ActionItem.(fyne.Disableable).Disable() + } + } +} + +func (e *SelectEntry) popUpPos() fyne.Position { + entryPos := fyne.CurrentApp().Driver().AbsolutePositionForObject(e.super()) + return entryPos.Add(fyne.NewPos(0, e.Size().Height-e.Theme().Size(theme.SizeNameInputBorder))) +} + +func (e *SelectEntry) setupDropDown() *Button { + dropDownButton := NewButton("", func() { + c := fyne.CurrentApp().Driver().CanvasForObject(e.super()) + + e.popUp = NewPopUpMenu(e.dropDown, c) + e.popUp.ShowAtPosition(e.popUpPos()) + e.popUp.Resize(fyne.NewSize(e.Size().Width, e.popUp.MinSize().Height)) + }) + dropDownButton.Importance = LowImportance + dropDownButton.SetIcon(e.Theme().Icon(theme.IconNameArrowDropDown)) + return dropDownButton +} diff --git a/vendor/fyne.io/fyne/v2/widget/selectable.go b/vendor/fyne.io/fyne/v2/widget/selectable.go new file mode 100644 index 0000000..fd00139 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/selectable.go @@ -0,0 +1,415 @@ +package widget + +import ( + "math" + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/driver/desktop" + "fyne.io/fyne/v2/driver/mobile" + "fyne.io/fyne/v2/lang" + "fyne.io/fyne/v2/theme" +) + +type selectable struct { + BaseWidget + cursorRow, cursorColumn int + + // selectRow and selectColumn represent the selection start location + // The selection will span from selectRow/Column to CursorRow/Column -- note that the cursor + // position may occur before or after the select start position in the text. + selectRow, selectColumn int + + focussed, selecting, selectEnded, password bool + sizeName fyne.ThemeSizeName + style fyne.TextStyle + + provider *RichText + theme fyne.Theme + focus fyne.Focusable + + // doubleTappedAtUnixMillis stores the time the entry was last DoubleTapped + // used for deciding whether the next MouseDown/TouchDown is a triple-tap or not + doubleTappedAtUnixMillis int64 +} + +func (s *selectable) CreateRenderer() fyne.WidgetRenderer { + return &selectableRenderer{sel: s} +} + +func (s *selectable) Cursor() desktop.Cursor { + return desktop.TextCursor +} + +func (s *selectable) DoubleTapped(p *fyne.PointEvent) { + s.doubleTappedAtUnixMillis = time.Now().UnixMilli() + s.updateMousePointer(p.Position) + row := s.provider.row(s.cursorRow) + start, end := getTextWhitespaceRegion(row, s.cursorColumn, false) + if start == -1 || end == -1 { + return + } + + s.selectRow = s.cursorRow + s.selectColumn = start + s.cursorColumn = end + + s.selecting = true + s.grabFocus() + s.Refresh() +} + +func (s *selectable) DragEnd() { + if s.cursorColumn == s.selectColumn && s.cursorRow == s.selectRow { + s.selecting = false + } + + shouldRefresh := !s.selecting + if shouldRefresh { + s.Refresh() + } + s.selectEnded = true +} + +func (s *selectable) Dragged(d *fyne.DragEvent) { + s.dragged(d) +} + +func (s *selectable) dragged(d *fyne.DragEvent) { + if !s.selecting || s.selectEnded { + s.selectEnded = false + s.updateMousePointer(d.Position) + + startPos := d.Position.Subtract(d.Dragged) + s.selectRow, s.selectColumn = s.getRowCol(startPos) + s.selecting = true + + s.grabFocus() + } + + s.updateMousePointer(d.Position) + s.Refresh() +} + +func (s *selectable) MouseDown(m *desktop.MouseEvent) { + if isTripleTap(s.doubleTappedAtUnixMillis, time.Now().UnixMilli()) { + s.selectCurrentRow(false) + return + } + s.grabFocus() + if s.selecting && m.Button == desktop.MouseButtonPrimary { + s.selecting = false + } +} + +func (s *selectable) MouseUp(ev *desktop.MouseEvent) { + if ev.Button == desktop.MouseButtonSecondary { + return + } + + start, _ := s.selection() + if (start == -1 || (s.selectRow == s.cursorRow && s.selectColumn == s.cursorColumn)) && s.selecting { + s.selecting = false + } + s.Refresh() +} + +// SelectedText returns the text currently selected in this Entry. +// If there is no selection it will return the empty string. +func (s *selectable) SelectedText() string { + if s == nil || !s.selecting { + return "" + } + + start, stop := s.selection() + if start == stop { + return "" + } + r := ([]rune)(s.provider.String()) + return string(r[start:stop]) +} + +func (s *selectable) Tapped(*fyne.PointEvent) { + if !fyne.CurrentDevice().IsMobile() { + return + } + + if s.doubleTappedAtUnixMillis != 0 { + s.doubleTappedAtUnixMillis = 0 + return // was a triple (TappedDouble plus Tapped) + } + s.selecting = false + s.Refresh() +} + +func (s *selectable) TappedSecondary(ev *fyne.PointEvent) { + app := fyne.CurrentApp() + c := app.Driver().CanvasForObject(s.focus.(fyne.CanvasObject)) + if c == nil { + return + } + + m := fyne.NewMenu("", + fyne.NewMenuItem(lang.L("Copy"), func() { + app.Clipboard().SetContent(s.SelectedText()) + })) + ShowPopUpMenuAtPosition(m, c, ev.AbsolutePosition) +} + +func (s *selectable) TouchCancel(m *mobile.TouchEvent) { + s.TouchUp(m) +} + +func (s *selectable) TouchDown(m *mobile.TouchEvent) { + if isTripleTap(s.doubleTappedAtUnixMillis, time.Now().UnixMilli()) { + s.selectCurrentRow(true) + return + } +} + +func (s *selectable) TouchUp(*mobile.TouchEvent) { +} + +func (s *selectable) TypedShortcut(sh fyne.Shortcut) { + switch sh.(type) { + case *fyne.ShortcutCopy: + fyne.CurrentApp().Clipboard().SetContent(s.SelectedText()) + } +} + +func (s *selectable) cursorColAt(text []rune, pos fyne.Position) int { + th := s.theme + textSize := th.Size(s.getSizeName()) + innerPad := th.Size(theme.SizeNameInnerPadding) + + for i := 0; i < len(text); i++ { + str := string(text[0:i]) + wid := fyne.MeasureText(str, textSize, s.style).Width + charWid := fyne.MeasureText(string(text[i]), textSize, s.style).Width + if pos.X < innerPad+wid+(charWid/2) { + return i + } + } + return len(text) +} + +func (s *selectable) getRowCol(p fyne.Position) (int, int) { + th := s.theme + textSize := th.Size(s.getSizeName()) + innerPad := th.Size(theme.SizeNameInnerPadding) + + rowHeight := s.provider.charMinSize(false, s.style, textSize).Height // TODO handle Password + row := int(math.Floor(float64(p.Y-innerPad+th.Size(theme.SizeNameLineSpacing)) / float64(rowHeight))) + col := 0 + if row < 0 { + row = 0 + } else if row >= s.provider.rows() { + row = s.provider.rows() - 1 + col = s.provider.rowLength(row) + } else { + col = s.cursorColAt(s.provider.row(row), p) + } + + return row, col +} + +// Selects the row where the cursorColumn is currently positioned +func (s *selectable) selectCurrentRow(focus bool) { + s.grabFocus() + provider := s.provider + s.selectRow = s.cursorRow + s.selectColumn = 0 + s.cursorColumn = provider.rowLength(s.cursorRow) + s.Refresh() +} + +// selection returns the start and end text positions for the selected span of text +// Note: this functionality depends on the relationship between the selection start row/col and +// the current cursor row/column. +// eg: (whitespace for clarity, '_' denotes cursor) +// +// "T e s [t i]_n g" == 3, 5 +// "T e s_[t i] n g" == 3, 5 +// "T e_[s t i] n g" == 2, 5 +func (s *selectable) selection() (int, int) { + noSelection := !s.selecting || (s.cursorRow == s.selectRow && s.cursorColumn == s.selectColumn) + + if noSelection { + return -1, -1 + } + + // Find the selection start + rowA, colA := s.cursorRow, s.cursorColumn + rowB, colB := s.selectRow, s.selectColumn + // Reposition if the cursors row is more than select start row, or if the row is the same and + // the cursors col is more that the select start column + if rowA > s.selectRow || (rowA == s.selectRow && colA > s.selectColumn) { + rowA, colA = s.selectRow, s.selectColumn + rowB, colB = s.cursorRow, s.cursorColumn + } + + return textPosFromRowCol(rowA, colA, s.provider), textPosFromRowCol(rowB, colB, s.provider) +} + +// Obtains textual position from a given row and col +// expects a read or write lock to be held by the caller +func textPosFromRowCol(row, col int, prov *RichText) int { + b := prov.rowBoundary(row) + if b == nil { + return col + } + return b.begin + col +} + +func (s *selectable) updateMousePointer(p fyne.Position) { + row, col := s.getRowCol(p) + s.cursorRow, s.cursorColumn = row, col + + if !s.selecting { + s.selectRow = row + s.selectColumn = col + } +} + +func (s *selectable) getSizeName() fyne.ThemeSizeName { + if s.sizeName != "" { + return s.sizeName + } + return theme.SizeNameText +} + +type selectableRenderer struct { + sel *selectable + + selections []fyne.CanvasObject +} + +func (r *selectableRenderer) Destroy() { +} + +func (r *selectableRenderer) Layout(fyne.Size) { +} + +func (r *selectableRenderer) MinSize() fyne.Size { + return fyne.Size{} +} + +func (r *selectableRenderer) Objects() []fyne.CanvasObject { + return r.selections +} + +func (r *selectableRenderer) Refresh() { + r.buildSelection() + selections := r.selections + v := fyne.CurrentApp().Settings().ThemeVariant() + + selectionColor := r.sel.theme.Color(theme.ColorNameSelection, v) + for _, selection := range selections { + rect := selection.(*canvas.Rectangle) + rect.FillColor = selectionColor + + if r.sel.focussed { + rect.Show() + } else { + rect.Hide() + } + } + + canvas.Refresh(r.sel.impl) +} + +// This process builds a slice of rectangles: +// - one entry per row of text +// - ordered by row order as they occur in multiline text +// This process could be optimized in the scenario where the user is selecting upwards: +// If the upwards case instead produces an order-reversed slice then only the newest rectangle would +// require movement and resizing. The existing solution creates a new rectangle and then moves/resizes +// all rectangles to comply with the occurrence order as stated above. +func (r *selectableRenderer) buildSelection() { + th := r.sel.theme + v := fyne.CurrentApp().Settings().ThemeVariant() + textSize := th.Size(r.sel.getSizeName()) + + cursorRow, cursorCol := r.sel.cursorRow, r.sel.cursorColumn + selectRow, selectCol := -1, -1 + if r.sel.selecting { + selectRow = r.sel.selectRow + selectCol = r.sel.selectColumn + } + + if selectRow == -1 || (cursorRow == selectRow && cursorCol == selectCol) { + r.selections = r.selections[:0] + return + } + + provider := r.sel.provider + innerPad := th.Size(theme.SizeNameInnerPadding) + // Convert column, row into x,y + getCoordinates := func(column int, row int) (float32, float32) { + sz := provider.lineSizeToColumn(column, row, textSize, innerPad) + return sz.Width, sz.Height*float32(row) - th.Size(theme.SizeNameInputBorder) + innerPad + } + + lineHeight := r.sel.provider.charMinSize(r.sel.password, r.sel.style, textSize).Height + + minmax := func(a, b int) (int, int) { + if a < b { + return a, b + } + return b, a + } + + // The remainder of the function calculates the set of boxes and add them to r.selection + + selectStartRow, selectEndRow := minmax(selectRow, cursorRow) + selectStartCol, selectEndCol := minmax(selectCol, cursorCol) + if selectRow < cursorRow { + selectStartCol, selectEndCol = selectCol, cursorCol + } + if selectRow > cursorRow { + selectStartCol, selectEndCol = cursorCol, selectCol + } + rowCount := selectEndRow - selectStartRow + 1 + + // trim r.selection to remove unwanted old rectangles + if len(r.selections) > rowCount { + r.selections = r.selections[:rowCount] + } + + // build a rectangle for each row and add it to r.selection + for i := 0; i < rowCount; i++ { + if len(r.selections) <= i { + box := canvas.NewRectangle(th.Color(theme.ColorNameSelection, v)) + r.selections = append(r.selections, box) + } + + // determine starting/ending columns for this rectangle + row := selectStartRow + i + startCol, endCol := selectStartCol, selectEndCol + if selectStartRow < row { + startCol = 0 + } + if selectEndRow > row { + endCol = provider.rowLength(row) + } + + // translate columns and row into draw coordinates + x1, y1 := getCoordinates(startCol, row) + x2, _ := getCoordinates(endCol, row) + + // resize and reposition each rectangle + r.selections[i].Resize(fyne.NewSize(x2-x1+1, lineHeight)) + r.selections[i].Move(fyne.NewPos(x1-1, y1)) + } +} + +func (s *selectable) grabFocus() { + if c := fyne.CurrentApp().Driver().CanvasForObject(s.focus.(fyne.CanvasObject)); c != nil { + c.Focus(s.focus) + } +} + +func isTripleTap(double, nowMilli int64) bool { + return nowMilli-double <= fyne.CurrentApp().Driver().DoubleTapDelay().Milliseconds() +} diff --git a/vendor/fyne.io/fyne/v2/widget/separator.go b/vendor/fyne.io/fyne/v2/widget/separator.go new file mode 100644 index 0000000..4dd93bf --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/separator.go @@ -0,0 +1,79 @@ +package widget + +import ( + "image/color" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/theme" +) + +var _ fyne.Widget = (*Separator)(nil) + +// Separator is a widget for displaying a separator with themeable color. +// +// Since: 1.4 +type Separator struct { + BaseWidget + + invert bool +} + +// NewSeparator creates a new separator. +// +// Since: 1.4 +func NewSeparator() *Separator { + s := &Separator{} + s.ExtendBaseWidget(s) + return s +} + +// CreateRenderer returns a new renderer for the separator. +func (s *Separator) CreateRenderer() fyne.WidgetRenderer { + s.ExtendBaseWidget(s) + th := s.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + var col color.Color + if s.invert { + col = th.Color(theme.ColorNameForeground, v) + } else { + col = th.Color(theme.ColorNameSeparator, v) + } + bar := canvas.NewRectangle(col) + + return &separatorRenderer{ + WidgetRenderer: NewSimpleRenderer(bar), + bar: bar, + d: s, + } +} + +// MinSize returns the minimal size of the separator. +func (s *Separator) MinSize() fyne.Size { + s.ExtendBaseWidget(s) + return s.BaseWidget.MinSize() +} + +var _ fyne.WidgetRenderer = (*separatorRenderer)(nil) + +type separatorRenderer struct { + fyne.WidgetRenderer + bar *canvas.Rectangle + d *Separator +} + +func (r *separatorRenderer) MinSize() fyne.Size { + return fyne.NewSquareSize(r.d.Theme().Size(theme.SizeNameSeparatorThickness)) +} + +func (r *separatorRenderer) Refresh() { + th := r.d.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + if r.d.invert { + r.bar.FillColor = th.Color(theme.ColorNameForeground, v) + } else { + r.bar.FillColor = th.Color(theme.ColorNameSeparator, v) + } + canvas.Refresh(r.d) +} diff --git a/vendor/fyne.io/fyne/v2/widget/slider.go b/vendor/fyne.io/fyne/v2/widget/slider.go new file mode 100644 index 0000000..8d87017 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/slider.go @@ -0,0 +1,552 @@ +package widget + +import ( + "fmt" + "image/color" + "math" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/data/binding" + "fyne.io/fyne/v2/driver/desktop" + "fyne.io/fyne/v2/internal/widget" + "fyne.io/fyne/v2/theme" +) + +var ( + _ fyne.Draggable = (*Slider)(nil) + _ fyne.Focusable = (*Slider)(nil) + _ desktop.Hoverable = (*Slider)(nil) + _ fyne.Tappable = (*Slider)(nil) + _ fyne.Disableable = (*Slider)(nil) +) + +// Slider is a widget that can slide between two fixed values. +type Slider struct { + BaseWidget + + Value float64 + Min float64 + Max float64 + Step float64 + + Orientation Orientation + OnChanged func(float64) `json:"-"` + + // Since: 2.4 + OnChangeEnded func(float64) `json:"-"` + + binder basicBinder + hovered bool + focused bool + disabled bool // don't use DisableableWidget so we can put Since comments on funcs + pendingChange bool // true if value changed since last OnChangeEnded +} + +// NewSlider returns a basic slider. +func NewSlider(min, max float64) *Slider { + slider := &Slider{ + Value: 0, + Min: min, + Max: max, + Step: 1, + Orientation: Horizontal, + } + slider.ExtendBaseWidget(slider) + return slider +} + +// NewSliderWithData returns a slider connected with the specified data source. +// +// Since: 2.0 +func NewSliderWithData(min, max float64, data binding.Float) *Slider { + slider := NewSlider(min, max) + slider.Bind(data) + + return slider +} + +// Bind connects the specified data source to this Slider. +// The current value will be displayed and any changes in the data will cause the widget to update. +// User interactions with this Slider will set the value into the data source. +// +// Since: 2.0 +func (s *Slider) Bind(data binding.Float) { + s.binder.SetCallback(s.updateFromData) + s.binder.Bind(data) + + s.OnChanged = func(_ float64) { + s.binder.CallWithData(s.writeData) + } +} + +// DragEnd is called when the drag ends. +func (s *Slider) DragEnd() { + if !s.disabled { + s.fireChangeEnded() + } +} + +// Dragged is called when a drag event occurs. +func (s *Slider) Dragged(e *fyne.DragEvent) { + if s.disabled { + return + } + ratio := s.getRatio(&e.PointEvent) + lastValue := s.Value + + s.updateValue(ratio) + s.positionChanged(lastValue, s.Value) +} + +// Tapped is called when a pointer tapped event is captured. +// +// Since: 2.4 +func (s *Slider) Tapped(e *fyne.PointEvent) { + if s.disabled { + return + } + + if !s.focused { + focusIfNotMobile(s.super()) + } + + ratio := s.getRatio(e) + lastValue := s.Value + + s.updateValue(ratio) + s.positionChanged(lastValue, s.Value) + s.fireChangeEnded() +} + +func (s *Slider) positionChanged(lastValue, currentValue float64) { + if s.almostEqual(lastValue, currentValue) { + return + } + + s.Refresh() + + s.pendingChange = true + if s.OnChanged != nil { + s.OnChanged(s.Value) + } +} + +func (s *Slider) fireChangeEnded() { + if !s.pendingChange { + return + } + s.pendingChange = false + if s.OnChangeEnded != nil { + s.OnChangeEnded(s.Value) + } +} + +// FocusGained is called when this item gained the focus. +// +// Since: 2.4 +func (s *Slider) FocusGained() { + s.focused = true + if !s.disabled { + s.Refresh() + } +} + +// FocusLost is called when this item lost the focus. +// +// Since: 2.4 +func (s *Slider) FocusLost() { + s.focused = false + if !s.disabled { + s.Refresh() + } +} + +// MouseIn is called when a desktop pointer enters the widget. +// +// Since: 2.4 +func (s *Slider) MouseIn(_ *desktop.MouseEvent) { + s.hovered = true + if !s.disabled { + s.Refresh() + } +} + +// MouseMoved is called when a desktop pointer hovers over the widget. +// +// Since: 2.4 +func (s *Slider) MouseMoved(_ *desktop.MouseEvent) { +} + +// MouseOut is called when a desktop pointer exits the widget +// +// Since: 2.4 +func (s *Slider) MouseOut() { + s.hovered = false + if !s.disabled { + s.Refresh() + } +} + +// TypedKey is called when this item receives a key event. +// +// Since: 2.4 +func (s *Slider) TypedKey(key *fyne.KeyEvent) { + if s.disabled { + return + } + if s.Orientation == Vertical { + switch key.Name { + case fyne.KeyUp: + s.SetValue(s.Value + s.Step) + case fyne.KeyDown: + s.SetValue(s.Value - s.Step) + } + } else { + switch key.Name { + case fyne.KeyLeft: + s.SetValue(s.Value - s.Step) + case fyne.KeyRight: + s.SetValue(s.Value + s.Step) + } + } +} + +// TypedRune is called when this item receives a char event. +// +// Since: 2.4 +func (s *Slider) TypedRune(_ rune) { +} + +func (s *Slider) buttonDiameter(inlineIconSize float32) float32 { + return inlineIconSize - 4 // match radio icons +} + +func (s *Slider) endOffset(inlineIconSize, innerPadding float32) float32 { + return s.buttonDiameter(inlineIconSize)/2 + innerPadding - 1.5 // align with radio icons +} + +func (s *Slider) getRatio(e *fyne.PointEvent) float64 { + th := s.Theme() + pad := s.endOffset(th.Size(theme.SizeNameInlineIcon), th.Size(theme.SizeNameInnerPadding)) + + x := e.Position.X + y := e.Position.Y + size := s.Size() + + switch s.Orientation { + case Vertical: + if y > size.Height-pad { + return 0.0 + } else if y < pad { + return 1.0 + } else { + return 1 - float64(y-pad)/float64(size.Height-pad*2) + } + case Horizontal: + if x > size.Width-pad { + return 1.0 + } else if x < pad { + return 0.0 + } else { + return float64(x-pad) / float64(size.Width-pad*2) + } + } + return 0.0 +} + +func (s *Slider) clampValueToRange() { + if s.Value >= s.Max { + s.Value = s.Max + return + } else if s.Value <= s.Min { + s.Value = s.Min + return + } + + if s.Step == 0 { // extended Slider may not have this set - assume value is not adjusted + return + } + + // only work with positive mods so the maths holds up + value := s.Value + step := s.Step + invert := false + if s.Value < 0 { + invert = true + value = -value + } + + rem := math.Mod(value, step) + if rem == 0 { + return + } + min := value - rem + if rem > step/2 { + min += s.Step + } + + if invert { + s.Value = -min + } else { + s.Value = min + } +} + +func (s *Slider) updateValue(ratio float64) { + s.Value = s.Min + ratio*(s.Max-s.Min) + + s.clampValueToRange() +} + +// SetValue updates the value of the slider and clamps the value to be within the range. +func (s *Slider) SetValue(value float64) { + if s.Value == value { + return + } + + lastValue := s.Value + s.Value = value + + s.clampValueToRange() + s.positionChanged(lastValue, s.Value) + s.fireChangeEnded() +} + +// MinSize returns the size that this widget should not shrink below +func (s *Slider) MinSize() fyne.Size { + s.ExtendBaseWidget(s) + return s.BaseWidget.MinSize() +} + +// Disable disables the slider +// +// Since: 2.5 +func (s *Slider) Disable() { + if !s.disabled { + defer s.Refresh() + } + s.disabled = true +} + +// Enable enables the slider +// +// Since: 2.5 +func (s *Slider) Enable() { + if s.disabled { + defer s.Refresh() + } + s.disabled = false +} + +// Disabled returns true if the slider is currently disabled +// +// Since: 2.5 +func (s *Slider) Disabled() bool { + return s.disabled +} + +// CreateRenderer links this widget to its renderer. +func (s *Slider) CreateRenderer() fyne.WidgetRenderer { + s.ExtendBaseWidget(s) + th := s.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + track := canvas.NewRectangle(th.Color(theme.ColorNameInputBackground, v)) + active := canvas.NewRectangle(th.Color(theme.ColorNameForeground, v)) + thumb := &canvas.Circle{FillColor: th.Color(theme.ColorNameForeground, v)} + focusIndicator := &canvas.Circle{FillColor: color.Transparent} + + objects := []fyne.CanvasObject{track, active, thumb, focusIndicator} + + slide := &sliderRenderer{widget.NewBaseRenderer(objects), track, active, thumb, focusIndicator, s} + slide.Refresh() // prepare for first draw + return slide +} + +func (s *Slider) almostEqual(a, b float64) bool { + delta := math.Abs(a - b) + return delta <= s.Step/2 +} + +func (s *Slider) updateFromData(data binding.DataItem) { + if data == nil { + return + } + floatSource, ok := data.(binding.Float) + if !ok { + return + } + + val, err := floatSource.Get() + if err != nil { + fyne.LogError("Error getting current data value", err) + return + } + s.SetValue(val) // if val != s.Value, this will call updateFromData again, but only once +} + +func (s *Slider) writeData(data binding.DataItem) { + if data == nil { + return + } + floatTarget, ok := data.(binding.Float) + if !ok { + return + } + currentValue, err := floatTarget.Get() + if err != nil { + return + } + if s.Value != currentValue { + err := floatTarget.Set(s.Value) + if err != nil { + fyne.LogError(fmt.Sprintf("Failed to set binding value to %f", s.Value), err) + } + } +} + +// Unbind disconnects any configured data source from this Slider. +// The current value will remain at the last value of the data source. +// +// Since: 2.0 +func (s *Slider) Unbind() { + s.OnChanged = nil + s.binder.Unbind() +} + +const minLongSide = float32(34) // added to button diameter + +type sliderRenderer struct { + widget.BaseRenderer + track *canvas.Rectangle + active *canvas.Rectangle + thumb *canvas.Circle + focusIndicator *canvas.Circle + slider *Slider +} + +// Refresh updates the widget state for drawing. +func (s *sliderRenderer) Refresh() { + th := s.slider.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + s.track.FillColor = th.Color(theme.ColorNameInputBackground, v) + if s.slider.disabled { + s.thumb.FillColor = th.Color(theme.ColorNameDisabled, v) + } else { + s.thumb.FillColor = th.Color(theme.ColorNameForeground, v) + } + s.active.FillColor = s.thumb.FillColor + + if s.slider.focused && !s.slider.disabled { + s.focusIndicator.FillColor = th.Color(theme.ColorNameFocus, v) + } else if s.slider.hovered && !s.slider.disabled { + s.focusIndicator.FillColor = th.Color(theme.ColorNameHover, v) + } else { + s.focusIndicator.FillColor = color.Transparent + } + + s.focusIndicator.Refresh() + + s.slider.clampValueToRange() + s.Layout(s.slider.Size()) + canvas.Refresh(s.slider.super()) +} + +// Layout the components of the widget. +func (s *sliderRenderer) Layout(size fyne.Size) { + th := s.slider.Theme() + + inputBorderSize := th.Size(theme.SizeNameInputBorder) + trackWidth := inputBorderSize * 2 + inlineIconSize := th.Size(theme.SizeNameInlineIcon) + innerPadding := th.Size(theme.SizeNameInnerPadding) + diameter := s.slider.buttonDiameter(inlineIconSize) + endPad := s.slider.endOffset(inlineIconSize, innerPadding) + + var trackPos, activePos, thumbPos fyne.Position + var trackSize, activeSize fyne.Size + + // some calculations are relative to trackSize, so we must update that first + switch s.slider.Orientation { + case Vertical: + trackPos = fyne.NewPos(size.Width/2-inputBorderSize, endPad) + trackSize = fyne.NewSize(trackWidth, size.Height-endPad*2) + + case Horizontal: + trackPos = fyne.NewPos(endPad, size.Height/2-inputBorderSize) + trackSize = fyne.NewSize(size.Width-endPad*2, trackWidth) + } + s.track.Move(trackPos) + s.track.Resize(trackSize) + + activeOffset := s.getOffset(inlineIconSize, innerPadding) // TODO based on old size...0 + switch s.slider.Orientation { + case Vertical: + activePos = fyne.NewPos(trackPos.X, activeOffset) + activeSize = fyne.NewSize(trackWidth, trackSize.Height-activeOffset+endPad) + + thumbPos = fyne.NewPos( + trackPos.X-(diameter-trackSize.Width)/2, activeOffset-(diameter/2)) + case Horizontal: + activePos = trackPos + activeSize = fyne.NewSize(activeOffset-endPad, trackWidth) + + thumbPos = fyne.NewPos( + activeOffset-(diameter/2), trackPos.Y-(diameter-trackSize.Height)/2) + } + + s.active.Move(activePos) + s.active.Resize(activeSize) + + s.thumb.Move(thumbPos) + s.thumb.Resize(fyne.NewSize(diameter, diameter)) + + focusIndicatorSize := fyne.NewSquareSize(inlineIconSize + innerPadding) + delta := (focusIndicatorSize.Width - diameter) / 2 + s.focusIndicator.Resize(focusIndicatorSize) + s.focusIndicator.Move(thumbPos.SubtractXY(delta, delta)) +} + +// MinSize calculates the minimum size of a widget. +func (s *sliderRenderer) MinSize() fyne.Size { + th := s.slider.Theme() + pad := th.Size(theme.SizeNameInnerPadding) + tap := th.Size(theme.SizeNameInlineIcon) + dia := s.slider.buttonDiameter(tap) + s1, s2 := minLongSide+dia, tap+pad*2 + + switch s.slider.Orientation { + case Vertical: + return fyne.NewSize(s2, s1) + default: + return fyne.NewSize(s1, s2) + } +} + +func (s *sliderRenderer) getOffset(iconInlineSize, innerPadding float32) float32 { + endPad := s.slider.endOffset(iconInlineSize, innerPadding) + w := s.slider + size := s.track.Size() + if w.Value == w.Min || w.Min == w.Max { + switch w.Orientation { + case Vertical: + return size.Height + endPad + case Horizontal: + return endPad + } + } + ratio := float32((w.Value - w.Min) / (w.Max - w.Min)) + + switch w.Orientation { + case Vertical: + y := size.Height - ratio*size.Height + endPad + return y + case Horizontal: + x := ratio*size.Width + endPad + return x + } + + return endPad +} diff --git a/vendor/fyne.io/fyne/v2/widget/table.go b/vendor/fyne.io/fyne/v2/widget/table.go new file mode 100644 index 0000000..835ea72 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/table.go @@ -0,0 +1,1740 @@ +package widget + +import ( + "math" + "strconv" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/driver/desktop" + "fyne.io/fyne/v2/driver/mobile" + "fyne.io/fyne/v2/internal/async" + "fyne.io/fyne/v2/internal/widget" + "fyne.io/fyne/v2/theme" +) + +const noCellMatch = math.MaxInt + +var ( + // allTableCellsID represents all table cells when refreshing requested cells + allTableCellsID = TableCellID{-1, -1} + + // onlyNewTableCellsID represents newly visible table cells when refreshing requested cells + onlyNewTableCellsID = TableCellID{-2, -2} +) + +// Declare conformity with interfaces +var ( + _ desktop.Cursorable = (*Table)(nil) + _ fyne.Draggable = (*Table)(nil) + _ fyne.Focusable = (*Table)(nil) + _ desktop.Hoverable = (*Table)(nil) + _ fyne.Tappable = (*Table)(nil) + _ fyne.Widget = (*Table)(nil) +) + +// TableCellID is a type that represents a cell's position in a table based on its row and column location. +type TableCellID struct { + Row int + Col int +} + +// Table widget is a grid of items that can be scrolled and a cell selected. +// Its performance is provided by caching cell templates created with CreateCell and re-using them with UpdateCell. +// The size of the content rows/columns is returned by the Length callback. +// +// Since: 1.4 +type Table struct { + BaseWidget + + Length func() (rows int, cols int) `json:"-"` + CreateCell func() fyne.CanvasObject `json:"-"` + UpdateCell func(id TableCellID, template fyne.CanvasObject) `json:"-"` + OnSelected func(id TableCellID) `json:"-"` + OnUnselected func(id TableCellID) `json:"-"` + + // ShowHeaderRow specifies that a row should be added to the table with header content. + // This will default to an A-Z style content, unless overridden with `CreateHeader` and `UpdateHeader` calls. + // + // Since: 2.4 + ShowHeaderRow bool + + // ShowHeaderColumn specifies that a column should be added to the table with header content. + // This will default to an 1-10 style numeric content, unless overridden with `CreateHeader` and `UpdateHeader` calls. + // + // Since: 2.4 + ShowHeaderColumn bool + + // CreateHeader is an optional function that allows overriding of the default header widget. + // Developers must also override `UpdateHeader`. + // + // Since: 2.4 + CreateHeader func() fyne.CanvasObject `json:"-"` + + // UpdateHeader is used with `CreateHeader` to support custom header content. + // The `id` parameter will have `-1` value to indicate a header, and `> 0` where the column or row refer to data. + // + // Since: 2.4 + UpdateHeader func(id TableCellID, template fyne.CanvasObject) `json:"-"` + + // StickyRowCount specifies how many data rows should not scroll when the content moves. + // If `ShowHeaderRow` us `true` then the stuck row will appear immediately underneath. + // + // Since: 2.4 + StickyRowCount int + + // StickyColumnCount specifies how many data columns should not scroll when the content moves. + // If `ShowHeaderColumn` us `true` then the stuck column will appear immediately next to the header. + // + // Since: 2.4 + StickyColumnCount int + + // HideSeparators hides the separator lines between the table cells + // + // Since: 2.5 + HideSeparators bool + + currentFocus TableCellID + focused bool + selectedCell, hoveredCell *TableCellID + cells *tableCells + columnWidths, rowHeights map[int]float32 + moveCallback func() + offset fyne.Position + content *widget.Scroll + + cellSize, headerSize fyne.Size + stuckXOff, stuckYOff, stuckWidth, stuckHeight, dragStartSize float32 + top, left, corner, dividerLayer *clip + hoverHeaderRow, hoverHeaderCol, dragCol, dragRow int + dragStartPos fyne.Position +} + +// NewTable returns a new performant table widget defined by the passed functions. +// The first returns the data size in rows and columns, second parameter is a function that returns cell +// template objects that can be cached and the third is used to apply data at specified data location to the +// passed template CanvasObject. +// +// Since: 1.4 +func NewTable(length func() (rows int, cols int), create func() fyne.CanvasObject, update func(TableCellID, fyne.CanvasObject)) *Table { + t := &Table{Length: length, CreateCell: create, UpdateCell: update} + t.ExtendBaseWidget(t) + return t +} + +// NewTableWithHeaders returns a new performant table widget defined by the passed functions including sticky headers. +// The first returns the data size in rows and columns, second parameter is a function that returns cell +// template objects that can be cached and the third is used to apply data at specified data location to the +// passed template CanvasObject. +// The row and column headers will stick to the leading and top edges of the table and contain "1-10" and "A-Z" formatted labels. +// +// Since: 2.4 +func NewTableWithHeaders(length func() (rows int, cols int), create func() fyne.CanvasObject, update func(TableCellID, fyne.CanvasObject)) *Table { + t := NewTable(length, create, update) + t.ShowHeaderRow = true + t.ShowHeaderColumn = true + + return t +} + +// CreateRenderer returns a new renderer for the table. +func (t *Table) CreateRenderer() fyne.WidgetRenderer { + t.ExtendBaseWidget(t) + + t.headerSize = t.createHeader().MinSize() + if t.columnWidths != nil { + if v, ok := t.columnWidths[-1]; ok { + t.headerSize.Width = v + } + } + if t.rowHeights != nil { + if v, ok := t.rowHeights[-1]; ok { + t.headerSize.Height = v + } + } + t.cellSize = t.templateSize() + t.cells = newTableCells(t) + t.content = widget.NewScroll(t.cells) + t.top = newClip(t, &fyne.Container{}) + t.left = newClip(t, &fyne.Container{}) + t.corner = newClip(t, &fyne.Container{}) + t.dividerLayer = newClip(t, &fyne.Container{}) + t.dragCol = noCellMatch + t.dragRow = noCellMatch + + r := &tableRenderer{t: t} + r.SetObjects([]fyne.CanvasObject{t.top, t.left, t.corner, t.dividerLayer, t.content}) + t.content.OnScrolled = func(pos fyne.Position) { + t.offset = pos + t.cells.refreshForID(onlyNewTableCellsID) + } + + r.Layout(t.Size()) + return r +} + +func (t *Table) Cursor() desktop.Cursor { + if t.hoverHeaderRow != noCellMatch { + return desktop.VResizeCursor + } else if t.hoverHeaderCol != noCellMatch { + return desktop.HResizeCursor + } + + return desktop.DefaultCursor +} + +func (t *Table) Dragged(e *fyne.DragEvent) { + min := t.cellSize + col := t.dragCol + row := t.dragRow + startPos := t.dragStartPos + startSize := t.dragStartSize + + if col != noCellMatch { + newSize := startSize + (e.Position.X - startPos.X) + if newSize < min.Width { + newSize = min.Width + } + t.SetColumnWidth(t.dragCol, newSize) + } + if row != noCellMatch { + newSize := startSize + (e.Position.Y - startPos.Y) + if newSize < min.Height { + newSize = min.Height + } + t.SetRowHeight(t.dragRow, newSize) + } +} + +func (t *Table) DragEnd() { + t.dragCol = noCellMatch + t.dragRow = noCellMatch +} + +// FocusGained is called after this table has gained focus. +func (t *Table) FocusGained() { + t.focused = true + t.RefreshItem(t.currentFocus) +} + +// FocusLost is called after this Table has lost focus. +func (t *Table) FocusLost() { + t.focused = false + t.Refresh() // Item(t.currentFocus) +} + +func (t *Table) MouseIn(ev *desktop.MouseEvent) { + t.hoverAt(ev.Position) +} + +// MouseDown response to desktop mouse event +func (t *Table) MouseDown(e *desktop.MouseEvent) { + t.tapped(e.Position) +} + +func (t *Table) MouseMoved(ev *desktop.MouseEvent) { + t.hoverAt(ev.Position) +} + +func (t *Table) MouseOut() { + t.hoverOut() +} + +// MouseUp response to desktop mouse event +func (t *Table) MouseUp(*desktop.MouseEvent) { +} + +// RefreshItem refreshes a single item, specified by the item ID passed in. +// +// Since: 2.4 +func (t *Table) RefreshItem(id TableCellID) { + if t.cells == nil { + return + } + t.cells.refreshForID(id) +} + +// Select will mark the specified cell as selected. +func (t *Table) Select(id TableCellID) { + if t.Length == nil { + return + } + + rows, cols := t.Length() + if id.Row < 0 || id.Row >= rows || id.Col < 0 || id.Col >= cols { + return + } + + if t.selectedCell != nil && *t.selectedCell == id { + return + } + if f := t.OnUnselected; f != nil && t.selectedCell != nil { + f(*t.selectedCell) + } + t.selectedCell = &id + t.currentFocus = id + + t.ScrollTo(id) + + if f := t.OnSelected; f != nil { + f(id) + } +} + +// SetColumnWidth supports changing the width of the specified column. Columns normally take the width of the template +// cell returned from the CreateCell callback. The width parameter uses the same units as a fyne.Size type and refers +// to the internal content width not including the divider size. +// +// Since: 1.4.1 +func (t *Table) SetColumnWidth(id int, width float32) { + if id < 0 { + if t.headerSize.Width == width { + return + } + t.headerSize.Width = width + } + + if t.columnWidths == nil { + t.columnWidths = make(map[int]float32) + } + + if set, ok := t.columnWidths[id]; ok && set == width { + return + } + t.columnWidths[id] = width + t.Refresh() +} + +// SetRowHeight supports changing the height of the specified row. Rows normally take the height of the template +// cell returned from the CreateCell callback. The height parameter uses the same units as a fyne.Size type and refers +// to the internal content height not including the divider size. +// +// Since: 2.3 +func (t *Table) SetRowHeight(id int, height float32) { + if id < 0 { + if t.headerSize.Height == height { + return + } + t.headerSize.Height = height + } + + if t.rowHeights == nil { + t.rowHeights = make(map[int]float32) + } + + if set, ok := t.rowHeights[id]; ok && set == height { + return + } + t.rowHeights[id] = height + t.Refresh() +} + +// TouchDown response to mobile touch event +func (t *Table) TouchDown(e *mobile.TouchEvent) { + t.tapped(e.Position) +} + +// TouchUp response to mobile touch event +func (t *Table) TouchUp(*mobile.TouchEvent) { +} + +// TouchCancel response to mobile touch event +func (t *Table) TouchCancel(*mobile.TouchEvent) { +} + +// TypedKey is called if a key event happens while this Table is focused. +func (t *Table) TypedKey(event *fyne.KeyEvent) { + switch event.Name { + case fyne.KeySpace: + t.Select(t.currentFocus) + case fyne.KeyDown: + if f := t.Length; f != nil { + rows, _ := f() + if t.currentFocus.Row >= rows-1 { + return + } + } + t.RefreshItem(t.currentFocus) + t.currentFocus.Row++ + t.ScrollTo(t.currentFocus) + t.RefreshItem(t.currentFocus) + case fyne.KeyLeft: + if t.currentFocus.Col <= 0 { + return + } + t.RefreshItem(t.currentFocus) + t.currentFocus.Col-- + t.ScrollTo(t.currentFocus) + t.RefreshItem(t.currentFocus) + case fyne.KeyRight: + if f := t.Length; f != nil { + _, cols := f() + if t.currentFocus.Col >= cols-1 { + return + } + } + t.RefreshItem(t.currentFocus) + t.currentFocus.Col++ + t.ScrollTo(t.currentFocus) + t.RefreshItem(t.currentFocus) + case fyne.KeyUp: + if t.currentFocus.Row <= 0 { + return + } + t.RefreshItem(t.currentFocus) + t.currentFocus.Row-- + t.ScrollTo(t.currentFocus) + t.RefreshItem(t.currentFocus) + } +} + +// TypedRune is called if a text event happens while this Table is focused. +func (t *Table) TypedRune(_ rune) { + // intentionally left blank +} + +// Unselect will mark the cell provided by id as unselected. +func (t *Table) Unselect(id TableCellID) { + if t.selectedCell == nil || id != *t.selectedCell { + return + } + t.selectedCell = nil + + if t.moveCallback != nil { + t.moveCallback() + } + + if f := t.OnUnselected; f != nil { + f(id) + } +} + +// UnselectAll will mark all cells as unselected. +// +// Since: 2.1 +func (t *Table) UnselectAll() { + if t.selectedCell == nil { + return + } + + selected := *t.selectedCell + t.selectedCell = nil + + if t.moveCallback != nil { + t.moveCallback() + } + + if f := t.OnUnselected; f != nil { + f(selected) + } +} + +// ScrollTo will scroll to the given cell without changing the selection. +// Attempting to scroll beyond the limits of the table will scroll to +// the edge of the table instead. +// +// Since: 2.1 +func (t *Table) ScrollTo(id TableCellID) { + if t.Length == nil { + return + } + + if t.content == nil { + return + } + + rows, cols := t.Length() + if id.Row >= rows { + id.Row = rows - 1 + } + + if id.Col >= cols { + id.Col = cols - 1 + } + + scrollPos := t.offset + + cellX, cellWidth := t.findX(id.Col) + stickCols := t.StickyColumnCount + if stickCols > 0 { + cellX -= t.stuckXOff + t.stuckWidth + } + if t.ShowHeaderColumn { + cellX += t.headerSize.Width + stickCols-- + } + if stickCols == 0 || id.Col > stickCols { + if cellX < scrollPos.X { + scrollPos.X = cellX + } else if cellX+cellWidth > scrollPos.X+t.content.Size().Width { + scrollPos.X = cellX + cellWidth - t.content.Size().Width + } + } + + cellY, cellHeight := t.findY(id.Row) + stickRows := t.StickyRowCount + if stickRows > 0 { + cellY -= t.stuckYOff + t.stuckHeight + } + if t.ShowHeaderRow { + cellY += t.headerSize.Height + stickRows-- + } + if stickRows == 0 || id.Row >= stickRows { + if cellY < scrollPos.Y { + scrollPos.Y = cellY + } else if cellY+cellHeight > scrollPos.Y+t.content.Size().Height { + scrollPos.Y = cellY + cellHeight - t.content.Size().Height + } + } + + t.offset = scrollPos + t.content.Offset = scrollPos + t.content.Refresh() + t.finishScroll() +} + +// ScrollToBottom scrolls to the last row in the table +// +// Since: 2.1 +func (t *Table) ScrollToBottom() { + if t.Length == nil || t.content == nil { + return + } + + rows, _ := t.Length() + cellY, cellHeight := t.findY(rows - 1) + y := cellY + cellHeight - t.content.Size().Height + if y <= 0 { + return + } + + t.content.Offset.Y = y + t.offset.Y = y + t.finishScroll() +} + +// ScrollToLeading scrolls horizontally to the leading edge of the table +// +// Since: 2.1 +func (t *Table) ScrollToLeading() { + if t.content == nil { + return + } + + t.content.Offset.X = 0 + t.offset.X = 0 + t.finishScroll() +} + +// ScrollToOffset scrolls the table to a specific position +// +// Since: 2.6 +func (t *Table) ScrollToOffset(off fyne.Position) { + if t.content == nil { + return + } + + t.content.ScrollToOffset(off) + t.offset = t.content.Offset + t.finishScroll() +} + +// ScrollToTop scrolls to the first row in the table +// +// Since: 2.1 +func (t *Table) ScrollToTop() { + if t.content == nil { + return + } + + t.content.Offset.Y = 0 + t.offset.Y = 0 + t.finishScroll() +} + +// ScrollToTrailing scrolls horizontally to the trailing edge of the table +// +// Since: 2.1 +func (t *Table) ScrollToTrailing() { + if t.content == nil || t.Length == nil { + return + } + + _, cols := t.Length() + cellX, cellWidth := t.findX(cols - 1) + scrollX := cellX + cellWidth - t.content.Size().Width + if scrollX <= 0 { + return + } + + t.content.Offset.X = scrollX + t.offset.X = scrollX + t.finishScroll() +} + +func (t *Table) Tapped(e *fyne.PointEvent) { + if e.Position.X < 0 || e.Position.X >= t.Size().Width || e.Position.Y < 0 || e.Position.Y >= t.Size().Height { + t.selectedCell = nil + t.Refresh() + return + } + + col := t.columnAt(e.Position) + if col == noCellMatch || col < 0 { + return // out of col range + } + row := t.rowAt(e.Position) + if row == noCellMatch || row < 0 { + return // out of row range + } + t.Select(TableCellID{row, col}) + + if !fyne.CurrentDevice().IsMobile() { + t.RefreshItem(t.currentFocus) + canvas := fyne.CurrentApp().Driver().CanvasForObject(t) + if canvas != nil { + canvas.Focus(t.impl.(fyne.Focusable)) + } + t.RefreshItem(t.currentFocus) + } +} + +// columnAt returns a positive integer (or 0) for the column that is found at the `pos` X position. +// If the position is between cells the method will return a negative integer representing the next column, +// i.e. -1 means the gap between 0 and 1. +func (t *Table) columnAt(pos fyne.Position) int { + dataCols := 0 + if f := t.Length; f != nil { + _, dataCols = t.Length() + } + + visibleColWidths, offX, minCol, maxCol := t.visibleColumnWidths(t.cellSize.Width, dataCols) + i := minCol + end := maxCol + if pos.X < t.stuckXOff+t.stuckWidth { + offX = t.stuckXOff + end = t.StickyColumnCount + i = 0 + } else { + pos.X += t.content.Offset.X + offX += t.stuckXOff + } + padding := t.Theme().Size(theme.SizeNamePadding) + for x := offX; i < end; x += visibleColWidths[i-1] + padding { + if pos.X < x { + return -i // the space between i-1 and i + } else if pos.X < x+visibleColWidths[i] { + return i + } + i++ + } + return noCellMatch +} + +func (t *Table) createHeader() fyne.CanvasObject { + if f := t.CreateHeader; f != nil { + return f() + } + + l := NewLabel("00") + l.TextStyle.Bold = true + l.Alignment = fyne.TextAlignCenter + return l +} + +func (t *Table) findX(col int) (cellX float32, cellWidth float32) { + cellSize := t.templateSize() + padding := t.Theme().Size(theme.SizeNamePadding) + for i := 0; i <= col; i++ { + if cellWidth > 0 { + cellX += cellWidth + padding + } + + width := cellSize.Width + if w, ok := t.columnWidths[i]; ok { + width = w + } + cellWidth = width + } + return cellX, cellWidth +} + +func (t *Table) findY(row int) (cellY float32, cellHeight float32) { + cellSize := t.templateSize() + padding := t.Theme().Size(theme.SizeNamePadding) + for i := 0; i <= row; i++ { + if cellHeight > 0 { + cellY += cellHeight + padding + } + + height := cellSize.Height + if h, ok := t.rowHeights[i]; ok { + height = h + } + cellHeight = height + } + return cellY, cellHeight +} + +func (t *Table) finishScroll() { + if t.moveCallback != nil { + t.moveCallback() + } + t.cells.Refresh() +} + +func (t *Table) hoverAt(pos fyne.Position) { + col := t.columnAt(pos) + row := t.rowAt(pos) + t.hoveredCell = &TableCellID{row, col} + overHeaderRow := t.ShowHeaderRow && pos.Y < t.headerSize.Height + overHeaderCol := t.ShowHeaderColumn && pos.X < t.headerSize.Width + if overHeaderRow && !overHeaderCol { + if col >= 0 { + t.hoverHeaderCol = noCellMatch + } else { + t.hoverHeaderCol = -col - 1 + } + } else { + t.hoverHeaderCol = noCellMatch + } + if overHeaderCol && !overHeaderRow { + if row >= 0 { + t.hoverHeaderRow = noCellMatch + } else { + t.hoverHeaderRow = -row - 1 + } + } else { + t.hoverHeaderRow = noCellMatch + } + + rows, cols := 0, 0 + if f := t.Length; f != nil { + rows, cols = t.Length() + } + if t.hoveredCell.Col >= cols || t.hoveredCell.Row >= rows || t.hoveredCell.Col < 0 || t.hoveredCell.Row < 0 { + t.hoverOut() + return + } + + if t.moveCallback != nil { + t.moveCallback() + } +} + +func (t *Table) hoverOut() { + t.hoveredCell = nil + + if t.moveCallback != nil { + t.moveCallback() + } +} + +// rowAt returns a positive integer (or 0) for the row that is found at the `pos` Y position. +// If the position is between cells the method will return a negative integer representing the next row, +// i.e. -1 means the gap between rows 0 and 1. +func (t *Table) rowAt(pos fyne.Position) int { + dataRows := 0 + if f := t.Length; f != nil { + dataRows, _ = t.Length() + } + + visibleRowHeights, offY, minRow, maxRow := t.visibleRowHeights(t.cellSize.Height, dataRows) + i := minRow + end := maxRow + if pos.Y < t.stuckYOff+t.stuckHeight { + offY = t.stuckYOff + end = t.StickyRowCount + i = 0 + } else { + pos.Y += t.content.Offset.Y + offY += t.stuckYOff + } + padding := t.Theme().Size(theme.SizeNamePadding) + for y := offY; i < end; y += visibleRowHeights[i-1] + padding { + if pos.Y < y { + return -i // the space between i-1 and i + } else if pos.Y >= y && pos.Y < y+visibleRowHeights[i] { + return i + } + i++ + } + return noCellMatch +} + +func (t *Table) tapped(pos fyne.Position) { + if t.dragCol == noCellMatch && t.dragRow == noCellMatch { + t.dragStartPos = pos + if t.hoverHeaderRow != noCellMatch { + t.dragCol = noCellMatch + t.dragRow = t.hoverHeaderRow + size, ok := t.rowHeights[t.hoverHeaderRow] + if !ok { + size = t.cellSize.Height + } + t.dragStartSize = size + } else if t.hoverHeaderCol != noCellMatch { + t.dragCol = t.hoverHeaderCol + t.dragRow = noCellMatch + size, ok := t.columnWidths[t.hoverHeaderCol] + if !ok { + size = t.cellSize.Width + } + t.dragStartSize = size + } + } +} + +func (t *Table) templateSize() fyne.Size { + if f := t.CreateCell; f != nil { + template := createItemAndApplyThemeScope(f, t) // don't use cache, we need new template + if !t.ShowHeaderRow && !t.ShowHeaderColumn { + return template.MinSize() + } + return template.MinSize().Max(t.createHeader().MinSize()) + } + + fyne.LogError("Missing CreateCell callback required for Table", nil) + return fyne.Size{} +} + +func (t *Table) updateHeader(id TableCellID, o fyne.CanvasObject) { + if f := t.UpdateHeader; f != nil { + f(id, o) + return + } + + l := o.(*Label) + if id.Row < 0 { + ids := []rune{'A' + rune(id.Col%26)} + pre := (id.Col - id.Col%26) / 26 + for pre > 0 { + ids = append([]rune{'A' - 1 + rune(pre%26)}, ids...) + pre = (pre - pre%26) / 26 + } + l.SetText(string(ids)) + } else if id.Col < 0 { + l.SetText(strconv.Itoa(id.Row + 1)) + } else { + l.SetText("") + } +} + +func (t *Table) stickyColumnWidths(colWidth float32, cols int) (visible []float32) { + if cols == 0 { + return []float32{} + } + + max := t.StickyColumnCount + if max > cols { + max = cols + } + + visible = make([]float32, max) + + if len(t.columnWidths) == 0 { + for i := 0; i < max; i++ { + visible[i] = colWidth + } + return visible + } + + for i := 0; i < max; i++ { + height := colWidth + + if h, ok := t.columnWidths[i]; ok { + height = h + } + + visible[i] = height + } + return visible +} + +func (t *Table) visibleColumnWidths(colWidth float32, cols int) (visible map[int]float32, offX float32, minCol, maxCol int) { + maxCol = cols + colOffset, headWidth := float32(0), float32(0) + isVisible := false + visible = make(map[int]float32) + + if t.content.Size().Width <= 0 { + return visible, offX, minCol, maxCol + } + + padding := t.Theme().Size(theme.SizeNamePadding) + stick := t.StickyColumnCount + size := t.Size() + + if len(t.columnWidths) == 0 { + paddedWidth := colWidth + padding + + offX = float32(math.Floor(float64(t.offset.X/paddedWidth))) * paddedWidth + minCol = int(math.Floor(float64(offX / paddedWidth))) + maxCol = int(math.Ceil(float64((t.offset.X + size.Width) / paddedWidth))) + + if minCol > cols-1 { + minCol = cols - 1 + } + if minCol < 0 { + minCol = 0 + } + + if maxCol > cols { + maxCol = cols + } + + visible = make(map[int]float32, maxCol-minCol+stick) + for i := minCol; i < maxCol; i++ { + visible[i] = colWidth + } + for i := 0; i < stick; i++ { + visible[i] = colWidth + } + return visible, offX, minCol, maxCol + } + + for i := 0; i < cols; i++ { + width := colWidth + if w, ok := t.columnWidths[i]; ok { + width = w + } + + if colOffset <= t.offset.X-width-padding { + // before visible content + } else if colOffset <= headWidth || colOffset <= t.offset.X { + minCol = i + offX = colOffset + isVisible = true + } + if colOffset < t.offset.X+size.Width { + maxCol = i + 1 + } else { + break + } + + colOffset += width + padding + if isVisible || i < stick { + visible[i] = width + } + } + return visible, offX, minCol, maxCol +} + +func (t *Table) stickyRowHeights(rowHeight float32, rows int) (visible []float32) { + if rows == 0 { + return []float32{} + } + + max := t.StickyRowCount + if max > rows { + max = rows + } + + visible = make([]float32, max) + + if len(t.rowHeights) == 0 { + for i := 0; i < max; i++ { + visible[i] = rowHeight + } + return visible + } + + for i := 0; i < max; i++ { + height := rowHeight + + if h, ok := t.rowHeights[i]; ok { + height = h + } + + visible[i] = height + } + return visible +} + +func (t *Table) visibleRowHeights(rowHeight float32, rows int) (visible map[int]float32, offY float32, minRow, maxRow int) { + maxRow = rows + rowOffset, headHeight := float32(0), float32(0) + isVisible := false + visible = make(map[int]float32) + + if t.content.Size().Height <= 0 { + return visible, offY, minRow, maxRow + } + + padding := t.Theme().Size(theme.SizeNamePadding) + stick := t.StickyRowCount + size := t.Size() + + if len(t.rowHeights) == 0 { + paddedHeight := rowHeight + padding + + offY = float32(math.Floor(float64(t.offset.Y/paddedHeight))) * paddedHeight + minRow = int(math.Floor(float64(offY / paddedHeight))) + maxRow = int(math.Ceil(float64((t.offset.Y + size.Height) / paddedHeight))) + + if minRow > rows-1 { + minRow = rows - 1 + } + if minRow < 0 { + minRow = 0 + } + + if maxRow > rows { + maxRow = rows + } + + visible = make(map[int]float32, maxRow-minRow+stick) + for i := minRow; i < maxRow; i++ { + visible[i] = rowHeight + } + for i := 0; i < stick; i++ { + visible[i] = rowHeight + } + return visible, offY, minRow, maxRow + } + + for i := 0; i < rows; i++ { + height := rowHeight + if h, ok := t.rowHeights[i]; ok { + height = h + } + + if rowOffset <= t.offset.Y-height-padding { + // before visible content + } else if rowOffset <= headHeight || rowOffset <= t.offset.Y { + minRow = i + offY = rowOffset + isVisible = true + } + if rowOffset < t.offset.Y+size.Height { + maxRow = i + 1 + } else { + break + } + + rowOffset += height + padding + if isVisible || i < stick { + visible[i] = height + } + } + return visible, offY, minRow, maxRow +} + +// Declare conformity with WidgetRenderer interface. +var _ fyne.WidgetRenderer = (*tableRenderer)(nil) + +type tableRenderer struct { + widget.BaseRenderer + t *Table +} + +func (t *tableRenderer) Layout(s fyne.Size) { + th := t.t.Theme() + + t.calculateHeaderSizes(th) + off := fyne.NewPos(t.t.stuckWidth, t.t.stuckHeight) + if t.t.ShowHeaderRow { + off.Y += t.t.headerSize.Height + } + if t.t.ShowHeaderColumn { + off.X += t.t.headerSize.Width + } + + t.t.content.Move(off) + t.t.content.Resize(s.SubtractWidthHeight(off.X, off.Y)) + + t.t.top.Move(fyne.NewPos(off.X, 0)) + t.t.top.Resize(fyne.NewSize(s.Width-off.X, off.Y)) + t.t.left.Move(fyne.NewPos(0, off.Y)) + t.t.left.Resize(fyne.NewSize(off.X, s.Height-off.Y)) + t.t.corner.Resize(fyne.NewSize(off.X, off.Y)) + + t.t.dividerLayer.Resize(s) + if t.t.HideSeparators { + t.t.dividerLayer.Hide() + } else { + t.t.dividerLayer.Show() + } +} + +func (t *tableRenderer) MinSize() fyne.Size { + sep := t.t.Theme().Size(theme.SizeNamePadding) + min := t.t.content.MinSize().Max(t.t.cellSize) + if t.t.ShowHeaderRow { + min.Height += t.t.headerSize.Height + sep + } + if t.t.ShowHeaderColumn { + min.Width += t.t.headerSize.Width + sep + } + if t.t.StickyRowCount > 0 { + for i := 0; i < t.t.StickyRowCount; i++ { + height := t.t.cellSize.Height + if h, ok := t.t.rowHeights[i]; ok { + height = h + } + + min.Height += height + sep + } + } + if t.t.StickyColumnCount > 0 { + for i := 0; i < t.t.StickyColumnCount; i++ { + width := t.t.cellSize.Width + if w, ok := t.t.columnWidths[i]; ok { + width = w + } + + min.Width += width + sep + } + } + return min +} + +func (t *tableRenderer) Refresh() { + th := t.t.Theme() + t.t.headerSize = t.t.createHeader().MinSize() + if t.t.columnWidths != nil { + if v, ok := t.t.columnWidths[-1]; ok { + t.t.headerSize.Width = v + } + } + if t.t.rowHeights != nil { + if v, ok := t.t.rowHeights[-1]; ok { + t.t.headerSize.Height = v + } + } + t.t.cellSize = t.t.templateSize() + t.calculateHeaderSizes(th) + + t.Layout(t.t.Size()) + t.t.cells.Refresh() +} + +func (t *tableRenderer) calculateHeaderSizes(th fyne.Theme) { + t.t.stuckXOff = 0 + t.t.stuckYOff = 0 + + if t.t.ShowHeaderRow { + t.t.stuckYOff = t.t.headerSize.Height + } + if t.t.ShowHeaderColumn { + t.t.stuckXOff = t.t.headerSize.Width + } + + separatorThickness := th.Size(theme.SizeNamePadding) + stickyColWidths := t.t.stickyColumnWidths(t.t.cellSize.Width, t.t.StickyColumnCount) + stickyRowHeights := t.t.stickyRowHeights(t.t.cellSize.Height, t.t.StickyRowCount) + + var stuckHeight float32 + for _, rowHeight := range stickyRowHeights { + stuckHeight += rowHeight + separatorThickness + } + t.t.stuckHeight = stuckHeight + var stuckWidth float32 + for _, colWidth := range stickyColWidths { + stuckWidth += colWidth + separatorThickness + } + t.t.stuckWidth = stuckWidth +} + +// Declare conformity with Widget interface. +var _ fyne.Widget = (*tableCells)(nil) + +type tableCells struct { + BaseWidget + t *Table + + nextRefreshCellsID TableCellID +} + +func newTableCells(t *Table) *tableCells { + c := &tableCells{t: t, nextRefreshCellsID: allTableCellsID} + c.ExtendBaseWidget(c) + return c +} + +func (c *tableCells) CreateRenderer() fyne.WidgetRenderer { + th := c.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + marker := canvas.NewRectangle(th.Color(theme.ColorNameSelection, v)) + marker.CornerRadius = th.Size(theme.SizeNameSelectionRadius) + hover := canvas.NewRectangle(th.Color(theme.ColorNameHover, v)) + hover.CornerRadius = th.Size(theme.SizeNameSelectionRadius) + + r := &tableCellsRenderer{ + cells: c, + visible: make(map[TableCellID]fyne.CanvasObject), headers: make(map[TableCellID]fyne.CanvasObject), + headRowBG: canvas.NewRectangle(th.Color(theme.ColorNameHeaderBackground, v)), headColBG: canvas.NewRectangle(theme.Color(theme.ColorNameHeaderBackground)), + headRowStickyBG: canvas.NewRectangle(th.Color(theme.ColorNameHeaderBackground, v)), headColStickyBG: canvas.NewRectangle(theme.Color(theme.ColorNameHeaderBackground)), + marker: marker, hover: hover, + } + + c.t.moveCallback = r.moveIndicators + return r +} + +func (c *tableCells) Resize(s fyne.Size) { + c.BaseWidget.Resize(s) + c.refreshForID(onlyNewTableCellsID) // trigger a redraw +} + +func (c *tableCells) refreshForID(id TableCellID) { + c.nextRefreshCellsID = id + c.BaseWidget.Refresh() +} + +func (c *tableCells) Refresh() { + c.nextRefreshCellsID = allTableCellsID + c.BaseWidget.Refresh() +} + +// Declare conformity with WidgetRenderer interface. +var _ fyne.WidgetRenderer = (*tableCellsRenderer)(nil) + +type tableCellsRenderer struct { + widget.BaseRenderer + + cells *tableCells + pool, headerPool async.Pool[fyne.CanvasObject] + visible, headers map[TableCellID]fyne.CanvasObject + hover, marker *canvas.Rectangle + dividers []fyne.CanvasObject + + headColBG, headRowBG, headRowStickyBG, headColStickyBG *canvas.Rectangle +} + +func (r *tableCellsRenderer) Layout(fyne.Size) { + r.moveIndicators() +} + +func (r *tableCellsRenderer) MinSize() fyne.Size { + rows, cols := 0, 0 + if f := r.cells.t.Length; f != nil { + rows, cols = r.cells.t.Length() + } else { + fyne.LogError("Missing Length callback required for Table", nil) + } + + stickRows := r.cells.t.StickyRowCount + stickCols := r.cells.t.StickyColumnCount + + width := float32(0) + if len(r.cells.t.columnWidths) == 0 { + width = r.cells.t.cellSize.Width * float32(cols-stickCols) + } else { + cellWidth := r.cells.t.cellSize.Width + for col := stickCols; col < cols; col++ { + colWidth, ok := r.cells.t.columnWidths[col] + if ok { + width += colWidth + } else { + width += cellWidth + } + } + } + + height := float32(0) + if len(r.cells.t.rowHeights) == 0 { + height = r.cells.t.cellSize.Height * float32(rows-stickRows) + } else { + cellHeight := r.cells.t.cellSize.Height + for row := stickRows; row < rows; row++ { + rowHeight, ok := r.cells.t.rowHeights[row] + if ok { + height += rowHeight + } else { + height += cellHeight + } + } + } + + separatorSize := r.cells.t.Theme().Size(theme.SizeNamePadding) + return fyne.NewSize(width+float32(cols-stickCols-1)*separatorSize, height+float32(rows-stickRows-1)*separatorSize) +} + +func (r *tableCellsRenderer) Refresh() { + r.refreshForID(r.cells.nextRefreshCellsID) +} + +func (r *tableCellsRenderer) refreshForID(toDraw TableCellID) { + th := r.cells.t.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + separatorThickness := th.Size(theme.SizeNamePadding) + dataRows, dataCols := 0, 0 + if f := r.cells.t.Length; f != nil { + dataRows, dataCols = r.cells.t.Length() + } + visibleColWidths, offX, minCol, maxCol := r.cells.t.visibleColumnWidths(r.cells.t.cellSize.Width, dataCols) + if len(visibleColWidths) == 0 && dataCols > 0 { // we can't show anything until we have some dimensions + return + } + visibleRowHeights, offY, minRow, maxRow := r.cells.t.visibleRowHeights(r.cells.t.cellSize.Height, dataRows) + if len(visibleRowHeights) == 0 && dataRows > 0 { // we can't show anything until we have some dimensions + return + } + + var cellXOffset, cellYOffset float32 + stickRows := r.cells.t.StickyRowCount + if r.cells.t.ShowHeaderRow { + cellYOffset += r.cells.t.headerSize.Height + } + stickCols := r.cells.t.StickyColumnCount + if r.cells.t.ShowHeaderColumn { + cellXOffset += r.cells.t.headerSize.Width + } + startRow := minRow + stickRows + if startRow < stickRows { + startRow = stickRows + } + startCol := minCol + stickCols + if startCol < stickCols { + startCol = stickCols + } + + wasVisible := r.visible + r.visible = make(map[TableCellID]fyne.CanvasObject) + var cells []fyne.CanvasObject + displayCol := func(row, col int, rowHeight float32, cells *[]fyne.CanvasObject) { + id := TableCellID{row, col} + colWidth := visibleColWidths[col] + c, ok := wasVisible[id] + if !ok { + c = r.pool.Get() + if f := r.cells.t.CreateCell; f != nil && c == nil { + c = createItemAndApplyThemeScope(f, r.cells.t) + } + if c == nil { + return + } + } + + c.Move(fyne.NewPos(cellXOffset, cellYOffset)) + c.Resize(fyne.NewSize(colWidth, rowHeight)) + + r.visible[id] = c + *cells = append(*cells, c) + cellXOffset += colWidth + separatorThickness + } + + displayRow := func(row int, cells *[]fyne.CanvasObject) { + rowHeight := visibleRowHeights[row] + cellXOffset = offX + + for col := startCol; col < maxCol; col++ { + displayCol(row, col, rowHeight, cells) + } + cellXOffset = r.cells.t.content.Offset.X + if r.cells.t.ShowHeaderColumn { + cellXOffset += r.cells.t.headerSize.Width + } + cellYOffset += rowHeight + separatorThickness + } + + cellYOffset = offY + for row := startRow; row < maxRow; row++ { + displayRow(row, &cells) + } + + inline := r.refreshHeaders(visibleRowHeights, visibleColWidths, offX, offY, startRow, maxRow, startCol, maxCol, + separatorThickness, th, v) + cells = append(cells, inline...) + + offX -= r.cells.t.content.Offset.X + cellYOffset = r.cells.t.stuckYOff + for row := 0; row < stickRows; row++ { + displayRow(row, &r.cells.t.top.Content.(*fyne.Container).Objects) + } + + cellYOffset = offY - r.cells.t.content.Offset.Y + for row := startRow; row < maxRow; row++ { + cellXOffset = r.cells.t.stuckXOff + rowHeight := visibleRowHeights[row] + for col := 0; col < stickCols; col++ { + displayCol(row, col, rowHeight, &r.cells.t.left.Content.(*fyne.Container).Objects) + } + cellYOffset += rowHeight + separatorThickness + } + + cellYOffset = r.cells.t.stuckYOff + for row := 0; row < stickRows; row++ { + cellXOffset = r.cells.t.stuckXOff + rowHeight := visibleRowHeights[row] + for col := 0; col < stickCols; col++ { + displayCol(row, col, rowHeight, &r.cells.t.corner.Content.(*fyne.Container).Objects) + } + cellYOffset += rowHeight + separatorThickness + } + + for id, old := range wasVisible { + if _, ok := r.visible[id]; !ok { + r.pool.Put(old) + } + } + + r.SetObjects(cells) + + r.updateCells(toDraw, r.visible, wasVisible) + for id, head := range r.headers { + r.cells.t.updateHeader(id, head) + } + + r.moveIndicators() + r.marker.FillColor = th.Color(theme.ColorNameSelection, v) + r.marker.CornerRadius = th.Size(theme.SizeNameSelectionRadius) + r.marker.Refresh() + r.hover.FillColor = th.Color(theme.ColorNameHover, v) + r.hover.CornerRadius = th.Size(theme.SizeNameSelectionRadius) + r.hover.Refresh() +} + +func (r *tableCellsRenderer) updateCells(toDraw TableCellID, visible, wasVisible map[TableCellID]fyne.CanvasObject) { + updateCell := r.cells.t.UpdateCell + if updateCell == nil { + fyne.LogError("Missing UpdateCell callback required for Table", nil) + return + } + + if toDraw == onlyNewTableCellsID { + for id, cell := range visible { + if _, ok := wasVisible[id]; !ok { + updateCell(id, cell) + } + } + return + } + + for id, cell := range visible { + if toDraw != allTableCellsID && toDraw != id { + continue + } + updateCell(id, cell) + } +} + +func (r *tableCellsRenderer) moveIndicators() { + rows, cols := 0, 0 + if f := r.cells.t.Length; f != nil { + rows, cols = r.cells.t.Length() + } + visibleColWidths, offX, minCol, maxCol := r.cells.t.visibleColumnWidths(r.cells.t.cellSize.Width, cols) + visibleRowHeights, offY, minRow, maxRow := r.cells.t.visibleRowHeights(r.cells.t.cellSize.Height, rows) + th := r.cells.t.Theme() + separatorThickness := th.Size(theme.SizeNameSeparatorThickness) + padding := th.Size(theme.SizeNamePadding) + dividerOff := (padding - separatorThickness) / 2 + + stickRows := r.cells.t.StickyRowCount + stickCols := r.cells.t.StickyColumnCount + + if r.cells.t.ShowHeaderColumn { + offX += r.cells.t.headerSize.Width + } + if r.cells.t.ShowHeaderRow { + offY += r.cells.t.headerSize.Height + } + if r.cells.t.selectedCell == nil { + r.moveMarker(r.marker, -1, -1, offX, offY, minCol, minRow, visibleColWidths, visibleRowHeights) + } else { + r.moveMarker(r.marker, r.cells.t.selectedCell.Row, r.cells.t.selectedCell.Col, offX, offY, minCol, minRow, visibleColWidths, visibleRowHeights) + } + if r.cells.t.hoveredCell == nil && !r.cells.t.focused { + r.moveMarker(r.hover, -1, -1, offX, offY, minCol, minRow, visibleColWidths, visibleRowHeights) + } else if r.cells.t.focused { + r.moveMarker(r.hover, r.cells.t.currentFocus.Row, r.cells.t.currentFocus.Col, offX, offY, minCol, minRow, visibleColWidths, visibleRowHeights) + } else { + r.moveMarker(r.hover, r.cells.t.hoveredCell.Row, r.cells.t.hoveredCell.Col, offX, offY, minCol, minRow, visibleColWidths, visibleRowHeights) + } + + colDivs := stickCols + maxCol - minCol - 1 + if colDivs < 0 { + colDivs = 0 + } + rowDivs := stickRows + maxRow - minRow - 1 + if rowDivs < 0 { + rowDivs = 0 + } + + if colDivs < 0 { + colDivs = 0 + } + if rowDivs < 0 { + rowDivs = 0 + } + + if len(r.dividers) < colDivs+rowDivs { + for i := len(r.dividers); i < colDivs+rowDivs; i++ { + r.dividers = append(r.dividers, NewSeparator()) + } + + objs := []fyne.CanvasObject{r.marker, r.hover} + r.cells.t.dividerLayer.Content.(*fyne.Container).Objects = append(objs, r.dividers...) + r.cells.t.dividerLayer.Content.Refresh() + } + + size := r.cells.t.Size() + + divs := 0 + i := 0 + if stickCols > 0 { + for x := r.cells.t.stuckXOff + visibleColWidths[i]; i < stickCols && divs < colDivs; x += visibleColWidths[i] + padding { + i++ + + xPos := x + dividerOff + r.dividers[divs].Resize(fyne.NewSize(separatorThickness, size.Height)) + r.dividers[divs].Move(fyne.NewPos(xPos, 0)) + r.dividers[divs].Show() + divs++ + } + } + i = minCol + stickCols + for x := offX + r.cells.t.stuckWidth + visibleColWidths[i]; i < maxCol-1 && divs < colDivs; x += visibleColWidths[i] + padding { + i++ + + xPos := x - r.cells.t.content.Offset.X + dividerOff + r.dividers[divs].Resize(fyne.NewSize(separatorThickness, size.Height)) + r.dividers[divs].Move(fyne.NewPos(xPos, 0)) + r.dividers[divs].Show() + divs++ + } + + i = 0 + if stickRows > 0 { + for y := r.cells.t.stuckYOff + visibleRowHeights[i]; i < stickRows && divs-colDivs < rowDivs; y += visibleRowHeights[i] + padding { + i++ + + yPos := y + dividerOff + r.dividers[divs].Resize(fyne.NewSize(size.Width, separatorThickness)) + r.dividers[divs].Move(fyne.NewPos(0, yPos)) + r.dividers[divs].Show() + divs++ + } + } + i = minRow + stickRows + for y := offY + r.cells.t.stuckHeight + visibleRowHeights[i]; i < maxRow-1 && divs-colDivs < rowDivs; y += visibleRowHeights[i] + padding { + i++ + + yPos := y - r.cells.t.content.Offset.Y + dividerOff + r.dividers[divs].Resize(fyne.NewSize(size.Width, separatorThickness)) + r.dividers[divs].Move(fyne.NewPos(0, yPos)) + r.dividers[divs].Show() + divs++ + } + + for i := divs; i < len(r.dividers); i++ { + r.dividers[i].Hide() + } +} + +func (r *tableCellsRenderer) moveMarker(marker fyne.CanvasObject, row, col int, offX, offY float32, minCol, minRow int, widths, heights map[int]float32) { + if col == -1 || row == -1 { + marker.Hide() + marker.Refresh() + return + } + + xPos := offX + stickCols := r.cells.t.StickyColumnCount + if col < stickCols { + if r.cells.t.ShowHeaderColumn { + xPos = r.cells.t.stuckXOff + } else { + xPos = 0 + } + minCol = 0 + } + + padding := r.cells.t.Theme().Size(theme.SizeNamePadding) + + for i := minCol; i < col; i++ { + xPos += widths[i] + xPos += padding + } + x1 := xPos + if col >= stickCols { + x1 -= r.cells.t.content.Offset.X + } + x2 := x1 + widths[col] + + yPos := offY + stickRows := r.cells.t.StickyRowCount + if row < stickRows { + if r.cells.t.ShowHeaderRow { + yPos = r.cells.t.stuckYOff + } else { + yPos = 0 + } + minRow = 0 + } + for i := minRow; i < row; i++ { + yPos += heights[i] + yPos += padding + } + y1 := yPos + if row >= stickRows { + y1 -= r.cells.t.content.Offset.Y + } + y2 := y1 + heights[row] + + size := r.cells.t.Size() + if x2 < 0 || x1 > size.Width || y2 < 0 || y1 > size.Height { + marker.Hide() + } else { + left := x1 + if col >= stickCols { // clip X + left = fyne.Max(r.cells.t.stuckXOff+r.cells.t.stuckWidth, x1) + } + top := y1 + if row >= stickRows { // clip Y + top = fyne.Max(r.cells.t.stuckYOff+r.cells.t.stuckHeight, y1) + } + marker.Move(fyne.NewPos(left, top)) + marker.Resize(fyne.NewSize(x2-left, y2-top)) + + marker.Show() + } + marker.Refresh() +} + +func (r *tableCellsRenderer) refreshHeaders(visibleRowHeights, visibleColWidths map[int]float32, offX, offY float32, + startRow, maxRow, startCol, maxCol int, separatorThickness float32, th fyne.Theme, v fyne.ThemeVariant, +) []fyne.CanvasObject { + wasVisible := r.headers + r.headers = make(map[TableCellID]fyne.CanvasObject) + headerMin := r.cells.t.headerSize + rowHeight := headerMin.Height + colWidth := headerMin.Width + + var cells, over []fyne.CanvasObject + corner := []fyne.CanvasObject{r.headColStickyBG, r.headRowStickyBG} + over = []fyne.CanvasObject{r.headRowBG} + if r.cells.t.ShowHeaderRow { + cellXOffset := offX - r.cells.t.content.Offset.X + displayColHeader := func(col int, list *[]fyne.CanvasObject) { + id := TableCellID{-1, col} + colWidth := visibleColWidths[col] + c, ok := wasVisible[id] + if !ok { + c = r.headerPool.Get() + if c == nil { + c = r.cells.t.createHeader() + } + if c == nil { + return + } + } + + c.Move(fyne.NewPos(cellXOffset, 0)) + c.Resize(fyne.NewSize(colWidth, rowHeight)) + + r.headers[id] = c + *list = append(*list, c) + cellXOffset += colWidth + separatorThickness + } + for col := startCol; col < maxCol; col++ { + displayColHeader(col, &over) + } + + if r.cells.t.StickyColumnCount > 0 { + cellXOffset = 0 + if r.cells.t.ShowHeaderColumn { + cellXOffset += r.cells.t.headerSize.Width + } + + for col := 0; col < r.cells.t.StickyColumnCount; col++ { + displayColHeader(col, &corner) + } + } + } + r.cells.t.top.Content.(*fyne.Container).Objects = over + r.cells.t.top.Content.Refresh() + + over = []fyne.CanvasObject{r.headColBG} + if r.cells.t.ShowHeaderColumn { + cellYOffset := offY - r.cells.t.content.Offset.Y + displayRowHeader := func(row int, list *[]fyne.CanvasObject) { + id := TableCellID{row, -1} + rowHeight := visibleRowHeights[row] + c, ok := wasVisible[id] + if !ok { + c = r.headerPool.Get() + if c == nil { + c = r.cells.t.createHeader() + } + if c == nil { + return + } + } + + c.Move(fyne.NewPos(0, cellYOffset)) + c.Resize(fyne.NewSize(colWidth, rowHeight)) + + r.headers[id] = c + *list = append(*list, c) + cellYOffset += rowHeight + separatorThickness + } + for row := startRow; row < maxRow; row++ { + displayRowHeader(row, &over) + } + + if r.cells.t.StickyRowCount > 0 { + cellYOffset = 0 + if r.cells.t.ShowHeaderRow { + cellYOffset += r.cells.t.headerSize.Height + } + + for row := 0; row < r.cells.t.StickyRowCount; row++ { + displayRowHeader(row, &corner) + } + } + } + r.cells.t.left.Content.(*fyne.Container).Objects = over + r.cells.t.left.Content.Refresh() + + r.headColBG.Hidden = !r.cells.t.ShowHeaderColumn + r.headColBG.FillColor = th.Color(theme.ColorNameHeaderBackground, v) + r.headColBG.Resize(fyne.NewSize(colWidth, r.cells.t.Size().Height)) + + r.headColStickyBG.Hidden = !r.cells.t.ShowHeaderColumn + r.headColStickyBG.FillColor = th.Color(theme.ColorNameHeaderBackground, v) + r.headColStickyBG.Resize(fyne.NewSize(colWidth, r.cells.t.stuckHeight+rowHeight)) + r.headRowBG.Hidden = !r.cells.t.ShowHeaderRow + r.headRowBG.FillColor = th.Color(theme.ColorNameHeaderBackground, v) + r.headRowBG.Resize(fyne.NewSize(r.cells.t.Size().Width, rowHeight)) + r.headRowStickyBG.Hidden = !r.cells.t.ShowHeaderRow + r.headRowStickyBG.FillColor = th.Color(theme.ColorNameHeaderBackground, v) + r.headRowStickyBG.Resize(fyne.NewSize(r.cells.t.stuckWidth+colWidth, rowHeight)) + r.cells.t.corner.Content.(*fyne.Container).Objects = corner + r.cells.t.corner.Content.Refresh() + + for id, old := range wasVisible { + if _, ok := r.headers[id]; !ok { + r.headerPool.Put(old) + } + } + return cells +} + +type clip struct { + widget.Scroll + + t *Table +} + +func newClip(t *Table, o fyne.CanvasObject) *clip { + c := &clip{t: t} + c.Content = o + c.Direction = widget.ScrollNone + + return c +} + +func (c *clip) DragEnd() { + c.t.DragEnd() + c.t.dragCol = noCellMatch + c.t.dragRow = noCellMatch +} + +func (c *clip) Dragged(e *fyne.DragEvent) { + c.t.Dragged(e) +} diff --git a/vendor/fyne.io/fyne/v2/widget/textgrid.go b/vendor/fyne.io/fyne/v2/widget/textgrid.go new file mode 100644 index 0000000..82846c0 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/textgrid.go @@ -0,0 +1,918 @@ +package widget + +import ( + "image/color" + "math" + "strconv" + "strings" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/internal/async" + "fyne.io/fyne/v2/internal/painter" + "fyne.io/fyne/v2/internal/widget" + "fyne.io/fyne/v2/theme" +) + +const ( + textAreaSpaceSymbol = '·' + textAreaTabSymbol = '→' + textAreaNewLineSymbol = '↵' +) + +var ( + // TextGridStyleDefault is a default style for test grid cells + TextGridStyleDefault TextGridStyle + // TextGridStyleWhitespace is the style used for whitespace characters, if enabled + TextGridStyleWhitespace TextGridStyle +) + +// TextGridCell represents a single cell in a text grid. +// It has a rune for the text content and a style associated with it. +type TextGridCell struct { + Rune rune + Style TextGridStyle +} + +// TextGridRow represents a row of cells cell in a text grid. +// It contains the cells for the row and an optional style. +type TextGridRow struct { + Cells []TextGridCell + Style TextGridStyle +} + +// TextGridStyle defines a style that can be applied to a TextGrid cell. +type TextGridStyle interface { + Style() fyne.TextStyle + TextColor() color.Color + BackgroundColor() color.Color +} + +// CustomTextGridStyle is a utility type for those not wanting to define their own style types. +type CustomTextGridStyle struct { + // Since: 2.5 + TextStyle fyne.TextStyle + FGColor, BGColor color.Color +} + +// TextColor is the color a cell should use for the text. +func (c *CustomTextGridStyle) TextColor() color.Color { + return c.FGColor +} + +// BackgroundColor is the color a cell should use for the background. +func (c *CustomTextGridStyle) BackgroundColor() color.Color { + return c.BGColor +} + +// Style is the text style a cell should use. +func (c *CustomTextGridStyle) Style() fyne.TextStyle { + return c.TextStyle +} + +// TextGrid is a monospaced grid of characters. +// This is designed to be used by a text editor, code preview or terminal emulator. +type TextGrid struct { + BaseWidget + Rows []TextGridRow + + scroll *widget.Scroll + content *textGridContent + + ShowLineNumbers bool + ShowWhitespace bool + TabWidth int // If set to 0 the fyne.DefaultTabWidth is used + + // Scroll can be used to turn off the scrolling of our TextGrid. + // + // Since: 2.6 + Scroll fyne.ScrollDirection +} + +// Append will add new lines to the end of this TextGrid. +// The first character will be at the beginning of a new line and any newline characters will split the text further. +// +// Since: 2.6 +func (t *TextGrid) Append(text string) { + rows := t.parseRows(text) + + t.Rows = append(t.Rows, rows...) + t.Refresh() +} + +// CursorLocationForPosition returns the location where a cursor would be if it was located in the cell under the +// requested position. If the grid is scrolled the position will refer to the visible offset and not the distance +// from the top left of the overall document. +// +// Since: 2.6 +func (t *TextGrid) CursorLocationForPosition(p fyne.Position) (row, col int) { + y := p.Y + x := p.X + + if t.scroll != nil && t.scroll.Visible() { + y += t.scroll.Offset.Y + x += t.scroll.Offset.X + } + + row = int(y / t.content.cellSize.Height) + col = int(x / t.content.cellSize.Width) + return row, col +} + +// ScrollToTop will scroll content to container top +// +// Since: 2.7 +func (t *TextGrid) ScrollToTop() { + t.scroll.ScrollToTop() + t.Refresh() +} + +// ScrollToBottom will scroll content to container bottom - to show latest info which end user just added +// +// Since: 2.7 +func (t *TextGrid) ScrollToBottom() { + t.scroll.ScrollToBottom() + t.Refresh() +} + +// PositionForCursorLocation returns the relative position in this TextGrid for the cell at position row, col. +// If the grid has been scrolled this will be taken into account so that the position compared to top left will +// refer to the requested location. +// +// Since: 2.6 +func (t *TextGrid) PositionForCursorLocation(row, col int) fyne.Position { + y := float32(row) * t.content.cellSize.Height + x := float32(col) * t.content.cellSize.Width + + if t.scroll != nil && t.scroll.Visible() { + y -= t.scroll.Offset.Y + x -= t.scroll.Offset.X + } + + return fyne.NewPos(x, y) +} + +// MinSize returns the smallest size this widget can shrink to +func (t *TextGrid) MinSize() fyne.Size { + t.ExtendBaseWidget(t) + return t.BaseWidget.MinSize() +} + +// Resize is called when this widget changes size. We should make sure that we refresh cells. +func (t *TextGrid) Resize(size fyne.Size) { + t.BaseWidget.Resize(size) + t.Refresh() +} + +// SetText updates the buffer of this textgrid to contain the specified text. +// New lines and columns will be added as required. Lines are separated by '\n'. +// The grid will use default text style and any previous content and style will be removed. +// Tab characters are padded with spaces to the next tab stop. +func (t *TextGrid) SetText(text string) { + rows := t.parseRows(text) + + oldRowsLen := len(t.Rows) + t.Rows = rows + + // If we don't update the scroll offset when the text is shorter, + // we may end up with no text displayed or text appearing partially cut off + if t.scroll != nil && t.Scroll != fyne.ScrollNone && len(rows) < oldRowsLen && t.scroll.Content != nil { + offset := t.PositionForCursorLocation(len(rows), 0) + t.scroll.ScrollToOffset(fyne.NewPos(offset.X, t.scroll.Offset.Y)) + t.scroll.Refresh() + } + + t.Refresh() +} + +// Text returns the contents of the buffer as a single string (with no style information). +// It reconstructs the lines by joining with a `\n` character. +// Tab characters have padded spaces removed. +func (t *TextGrid) Text() string { + count := len(t.Rows) - 1 // newlines + for _, row := range t.Rows { + count += len(row.Cells) + } + + if count <= 0 { + return "" + } + + runes := make([]rune, 0, count) + + for i, row := range t.Rows { + next := 0 + for col, cell := range row.Cells { + if col < next { + continue + } + runes = append(runes, cell.Rune) + if cell.Rune == '\t' { + next = nextTab(col, t.tabWidth()) + } + } + if i < len(t.Rows)-1 { + runes = append(runes, '\n') + } + } + + return string(runes) +} + +// Row returns a copy of the content in a specified row as a TextGridRow. +// If the index is out of bounds it returns an empty row object. +func (t *TextGrid) Row(row int) TextGridRow { + if row < 0 || row >= len(t.Rows) { + return TextGridRow{} + } + + return t.Rows[row] +} + +// RowText returns a string representation of the content at the row specified. +// If the index is out of bounds it returns an empty string. +func (t *TextGrid) RowText(row int) string { + rowData := t.Row(row) + count := len(rowData.Cells) + + if count <= 0 { + return "" + } + + runes := make([]rune, 0, count) + + next := 0 + for col, cell := range rowData.Cells { + if col < next { + continue + } + runes = append(runes, cell.Rune) + if cell.Rune == '\t' { + next = nextTab(col, t.tabWidth()) + } + } + return string(runes) +} + +// SetRow updates the specified row of the grid's contents using the specified content and style and then refreshes. +// If the row is beyond the end of the current buffer it will be expanded. +// Tab characters are not padded with spaces. +func (t *TextGrid) SetRow(row int, content TextGridRow) { + if row < 0 { + return + } + for len(t.Rows) <= row { + t.Rows = append(t.Rows, TextGridRow{}) + } + + t.Rows[row] = content + for col := 0; col > len(content.Cells); col++ { + t.refreshCell(row, col) + } +} + +// SetRowStyle sets a grid style to all the cells cell at the specified row. +// Any cells in this row with their own style will override this value when displayed. +func (t *TextGrid) SetRowStyle(row int, style TextGridStyle) { + if row < 0 { + return + } + for len(t.Rows) <= row { + t.Rows = append(t.Rows, TextGridRow{}) + } + t.Rows[row].Style = style +} + +// SetCell sets a grid data to the cell at named row and column. +func (t *TextGrid) SetCell(row, col int, cell TextGridCell) { + if row < 0 || col < 0 { + return + } + t.ensureCells(row, col) + + t.Rows[row].Cells[col] = cell + t.refreshCell(row, col) +} + +// SetRune sets a character to the cell at named row and column. +func (t *TextGrid) SetRune(row, col int, r rune) { + if row < 0 || col < 0 { + return + } + t.ensureCells(row, col) + + t.Rows[row].Cells[col].Rune = r + t.refreshCell(row, col) +} + +// SetStyle sets a grid style to the cell at named row and column. +func (t *TextGrid) SetStyle(row, col int, style TextGridStyle) { + if row < 0 || col < 0 { + return + } + t.ensureCells(row, col) + + t.Rows[row].Cells[col].Style = style + t.refreshCell(row, col) +} + +// SetStyleRange sets a grid style to all the cells between the start row and column through to the end row and column. +func (t *TextGrid) SetStyleRange(startRow, startCol, endRow, endCol int, style TextGridStyle) { + if startRow >= len(t.Rows) || endRow < 0 { + return + } + if startRow < 0 { + startRow = 0 + startCol = 0 + } + if endRow >= len(t.Rows) { + endRow = len(t.Rows) - 1 + endCol = len(t.Rows[endRow].Cells) - 1 + } + + if startRow == endRow { + for col := startCol; col <= endCol; col++ { + t.SetStyle(startRow, col, style) + } + return + } + + // first row + for col := startCol; col < len(t.Rows[startRow].Cells); col++ { + t.SetStyle(startRow, col, style) + } + + // possible middle rows + for rowNum := startRow + 1; rowNum < endRow; rowNum++ { + for col := 0; col < len(t.Rows[rowNum].Cells); col++ { + t.SetStyle(rowNum, col, style) + } + } + + // last row + for col := 0; col <= endCol; col++ { + t.SetStyle(endRow, col, style) + } +} + +// CreateRenderer is a private method to Fyne which links this widget to its renderer +func (t *TextGrid) CreateRenderer() fyne.WidgetRenderer { + t.ExtendBaseWidget(t) + + th := t.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + TextGridStyleDefault = &CustomTextGridStyle{} + TextGridStyleWhitespace = &CustomTextGridStyle{FGColor: th.Color(theme.ColorNameDisabled, v)} + + var scroll *widget.Scroll + content := newTextGridContent(t) + objs := make([]fyne.CanvasObject, 1) + if t.Scroll == widget.ScrollNone { + scroll = widget.NewScroll(nil) + objs[0] = content + } else { + scroll = widget.NewScroll(content) + scroll.Direction = t.Scroll + objs[0] = scroll + } + t.scroll = scroll + t.content = content + r := &textGridRenderer{text: content, scroll: scroll} + r.SetObjects(objs) + return r +} + +func (t *TextGrid) ensureCells(row, col int) { + for len(t.Rows) <= row { + t.Rows = append(t.Rows, TextGridRow{}) + } + data := t.Rows[row] + + for len(data.Cells) <= col { + data.Cells = append(data.Cells, TextGridCell{}) + t.Rows[row] = data + } +} + +func (t *TextGrid) parseRows(text string) []TextGridRow { + lines := strings.Split(text, "\n") + rows := make([]TextGridRow, len(lines)) + for i, line := range lines { + cells := make([]TextGridCell, 0, len(line)) + for _, r := range line { + cells = append(cells, TextGridCell{Rune: r}) + if r == '\t' { + col := len(cells) + next := nextTab(col-1, t.tabWidth()) + for i := col; i < next; i++ { + cells = append(cells, TextGridCell{Rune: ' '}) + } + } + } + rows[i] = TextGridRow{Cells: cells} + } + + return rows +} + +func (t *TextGrid) refreshCell(row, col int) { + r := t.content + r.refreshCell(row, col) +} + +// NewTextGrid creates a new empty TextGrid widget. +func NewTextGrid() *TextGrid { + grid := &TextGrid{} + grid.Scroll = widget.ScrollNone + grid.ExtendBaseWidget(grid) + return grid +} + +// NewTextGridFromString creates a new TextGrid widget with the specified string content. +func NewTextGridFromString(content string) *TextGrid { + grid := NewTextGrid() + grid.SetText(content) + return grid +} + +// nextTab finds the column of the next tab stop for the given column +func nextTab(column int, tabWidth int) int { + tabStop, _ := math.Modf(float64(column+tabWidth) / float64(tabWidth)) + return tabWidth * int(tabStop) +} + +type textGridRenderer struct { + widget.BaseRenderer + + text *textGridContent + scroll *widget.Scroll +} + +func (t *textGridRenderer) Layout(s fyne.Size) { + t.Objects()[0].Resize(s) +} + +func (t *textGridRenderer) MinSize() fyne.Size { + if t.text.text.Scroll == widget.ScrollNone { + return t.text.MinSize() + } + + return t.scroll.MinSize() +} + +func (t *textGridRenderer) Refresh() { + content := t.text + if t.text.text.Scroll != widget.ScrollNone { + t.scroll.Direction = t.text.text.Scroll + } + if t.text.text.Scroll == widget.ScrollNone && t.scroll.Content != nil { + t.scroll.Hide() + t.scroll.Content = nil + content.Resize(t.text.Size()) + t.SetObjects([]fyne.CanvasObject{t.text}) + } else if (t.text.text.Scroll != widget.ScrollNone) && t.scroll.Content == nil { + t.scroll.Content = content + t.scroll.Show() + + t.scroll.Resize(t.text.Size()) + content.Resize(content.MinSize()) + t.SetObjects([]fyne.CanvasObject{t.scroll}) + } + + canvas.Refresh(t.text.text.super()) + t.text.Refresh() +} + +type textGridContent struct { + BaseWidget + text *TextGrid + + rows int + cellSize fyne.Size + + visible []fyne.CanvasObject +} + +func newTextGridContent(t *TextGrid) *textGridContent { + grid := &textGridContent{text: t} + grid.ExtendBaseWidget(grid) + return grid +} + +// CreateRenderer is a private method to Fyne which links this widget to its renderer +func (t *textGridContent) CreateRenderer() fyne.WidgetRenderer { + r := &textGridContentRenderer{text: t} + + r.updateCellSize() + t.text.scroll.OnScrolled = func(_ fyne.Position) { + r.addRowsIfRequired() + r.Layout(t.Size()) + } + return r +} + +func (t *textGridContent) refreshCell(row, col int) { + if row >= len(t.visible)-1 { + return + } + wid := t.visible[row].(*textGridRow) + wid.refreshCell(col) +} + +type textGridContentRenderer struct { + text *textGridContent + itemPool async.Pool[*textGridRow] +} + +func (t *textGridContentRenderer) updateGridSize(size fyne.Size) { + bufRows := len(t.text.text.Rows) + sizeRows := int(size.Height / t.text.cellSize.Height) + + if sizeRows > bufRows { + t.text.rows = sizeRows + } else { + t.text.rows = bufRows + } + t.addRowsIfRequired() +} + +func (t *textGridContentRenderer) Destroy() { +} + +func (t *textGridContentRenderer) Layout(s fyne.Size) { + size := fyne.NewSize(s.Width, t.text.cellSize.Height) + t.updateGridSize(s) + + for _, o := range t.text.visible { + o.Move(fyne.NewPos(0, float32(o.(*textGridRow).row)*t.text.cellSize.Height)) + o.Resize(size) + } +} + +func (t *textGridContentRenderer) MinSize() fyne.Size { + longestRow := float32(0) + for _, row := range t.text.text.Rows { + longestRow = fyne.Max(longestRow, float32(len(row.Cells))) + } + return fyne.NewSize(t.text.cellSize.Width*longestRow, + t.text.cellSize.Height*float32(len(t.text.text.Rows))) +} + +func (t *textGridContentRenderer) Objects() []fyne.CanvasObject { + return t.text.visible +} + +func (t *textGridContentRenderer) Refresh() { + // theme could change text size + t.updateCellSize() + t.updateGridSize(t.text.text.Size()) + + for _, o := range t.text.visible { + o.Refresh() + } +} + +func (t *textGridContentRenderer) addRowsIfRequired() { + start := 0 + end := t.text.rows + if t.text.text.Scroll == widget.ScrollBoth || t.text.text.Scroll == widget.ScrollVerticalOnly { + off := t.text.text.scroll.Offset.Y + start = int(math.Floor(float64(off / t.text.cellSize.Height))) + + off += t.text.text.Size().Height + end = int(math.Ceil(float64(off / t.text.cellSize.Height))) + } + + remain := t.text.visible[:0] + for _, row := range t.text.visible { + if row.(*textGridRow).row < start || row.(*textGridRow).row > end { + t.itemPool.Put(row.(*textGridRow)) + continue + } + + remain = append(remain, row.(*textGridRow)) + } + t.text.visible = remain + + var newItems []fyne.CanvasObject + for i := start; i <= end; i++ { + found := false + for _, row := range t.text.visible { + if i == row.(*textGridRow).row { + found = true + break + } + } + + if found { + continue + } + + newRow := t.itemPool.Get() + if newRow == nil { + newRow = newTextGridRow(t.text, i) + } else { + newRow.setRow(i) + } + newItems = append(newItems, newRow) + } + + if len(newItems) > 0 { + t.text.visible = append(t.text.visible, newItems...) + } +} + +func (t *textGridContentRenderer) updateCellSize() { + th := t.text.Theme() + size := fyne.MeasureText("M", th.Size(theme.SizeNameText), fyne.TextStyle{Monospace: true}) + + // round it for seamless background + size.Width = float32(math.Round(float64(size.Width))) + size.Height = float32(math.Round(float64(size.Height))) + + t.text.cellSize = size +} + +type textGridRow struct { + BaseWidget + text *textGridContent + + objects []fyne.CanvasObject + row int + cols int + + cachedFGColor color.Color + cachedTextSize float32 +} + +func newTextGridRow(t *textGridContent, row int) *textGridRow { + newRow := &textGridRow{text: t, row: row} + newRow.ExtendBaseWidget(newRow) + + return newRow +} + +// CreateRenderer is a private method to Fyne which links this widget to its renderer +func (t *textGridRow) CreateRenderer() fyne.WidgetRenderer { + render := &textGridRowRenderer{obj: t} + + render.Refresh() // populate + return render +} + +func (t *textGridRow) setRow(row int) { + t.row = row + t.Refresh() +} + +func (t *textGridRow) appendTextCell(str rune) { + th := t.text.text.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + text := canvas.NewText(string(str), th.Color(theme.ColorNameForeground, v)) + text.TextStyle.Monospace = true + + bg := canvas.NewRectangle(color.Transparent) + + ul := canvas.NewLine(color.Transparent) + + t.objects = append(t.objects, bg, text, ul) +} + +func (t *textGridRow) refreshCell(col int) { + pos := t.cols + col + if pos*3+1 >= len(t.objects) { + return + } + + row := t.text.text.Rows[t.row] + + if len(row.Cells) > col { + cell := row.Cells[col] + t.setCellRune(cell.Rune, pos, cell.Style, row.Style) + } +} + +func (t *textGridRow) setCellRune(str rune, pos int, style, rowStyle TextGridStyle) { + if str == 0 { + str = ' ' + } + rect := t.objects[pos*3].(*canvas.Rectangle) + text := t.objects[pos*3+1].(*canvas.Text) + underline := t.objects[pos*3+2].(*canvas.Line) + + fg := t.cachedFGColor + text.TextSize = t.cachedTextSize + + var underlineStrokeWidth float32 = 1 + var underlineStrokeColor color.Color = color.Transparent + textStyle := fyne.TextStyle{} + if style != nil { + textStyle = style.Style() + } else if rowStyle != nil { + textStyle = rowStyle.Style() + } + if textStyle.Bold { + underlineStrokeWidth = 2 + } + if textStyle.Underline { + underlineStrokeColor = fg + } + textStyle.Monospace = true + + if style != nil && style.TextColor() != nil { + fg = style.TextColor() + } else if rowStyle != nil && rowStyle.TextColor() != nil { + fg = rowStyle.TextColor() + } + + newStr := string(str) + if text.Text != newStr || text.Color != fg || textStyle != text.TextStyle { + text.Text = newStr + text.Color = fg + text.TextStyle = textStyle + text.Refresh() + } + + if underlineStrokeWidth != underline.StrokeWidth || underlineStrokeColor != underline.StrokeColor { + underline.StrokeWidth, underline.StrokeColor = underlineStrokeWidth, underlineStrokeColor + underline.Refresh() + } + + bg := color.Color(color.Transparent) + if style != nil && style.BackgroundColor() != nil { + bg = style.BackgroundColor() + } else if rowStyle != nil && rowStyle.BackgroundColor() != nil { + bg = rowStyle.BackgroundColor() + } + if rect.FillColor != bg { + rect.FillColor = bg + rect.Refresh() + } +} + +func (t *textGridRow) addCellsIfRequired() { + cellCount := t.cols + if len(t.objects) == cellCount*3 { + return + } + for i := len(t.objects); i < cellCount*3; i += 3 { + t.appendTextCell(' ') + } +} + +func (t *textGridRow) refreshCells() { + x := 0 + if t.row >= len(t.text.text.Rows) { + for ; x < len(t.objects)/3; x++ { + t.setCellRune(' ', x, TextGridStyleDefault, nil) // blank rows no longer needed + } + + return // we can have more rows than content rows (filling space) + } + + row := t.text.text.Rows[t.row] + rowStyle := row.Style + i := 0 + if t.text.text.ShowLineNumbers { + lineStr := []rune(strconv.Itoa(t.row + 1)) + pad := t.lineNumberWidth() - len(lineStr) + for ; i < pad; i++ { + t.setCellRune(' ', x, TextGridStyleWhitespace, rowStyle) // padding space + x++ + } + for c := 0; c < len(lineStr); c++ { + t.setCellRune(lineStr[c], x, TextGridStyleDefault, rowStyle) // line numbers + i++ + x++ + } + + t.setCellRune('|', x, TextGridStyleWhitespace, rowStyle) // last space + i++ + x++ + } + for _, r := range row.Cells { + if i >= t.cols { // would be an overflow - bad + continue + } + if t.text.text.ShowWhitespace && (r.Rune == ' ' || r.Rune == '\t') { + sym := textAreaSpaceSymbol + if r.Rune == '\t' { + sym = textAreaTabSymbol + } + + if r.Style != nil && r.Style.BackgroundColor() != nil { + whitespaceBG := &CustomTextGridStyle{ + FGColor: TextGridStyleWhitespace.TextColor(), + BGColor: r.Style.BackgroundColor(), + } + t.setCellRune(sym, x, whitespaceBG, rowStyle) // whitespace char + } else { + t.setCellRune(sym, x, TextGridStyleWhitespace, rowStyle) // whitespace char + } + } else { + t.setCellRune(r.Rune, x, r.Style, rowStyle) // regular char + } + i++ + x++ + } + if t.text.text.ShowWhitespace && i < t.cols && t.row < len(t.text.text.Rows)-1 { + t.setCellRune(textAreaNewLineSymbol, x, TextGridStyleWhitespace, rowStyle) // newline + i++ + x++ + } + for ; i < t.cols; i++ { + t.setCellRune(' ', x, TextGridStyleDefault, rowStyle) // blanks + x++ + } + + for ; x < len(t.objects)/3; x++ { + t.setCellRune(' ', x, TextGridStyleDefault, nil) // trailing cells and blank lines + } +} + +// tabWidth either returns the set tab width or if not set the returns the DefaultTabWidth +func (t *TextGrid) tabWidth() int { + if t.TabWidth == 0 { + return painter.DefaultTabWidth + } + return t.TabWidth +} + +func (t *textGridRow) lineNumberWidth() int { + return len(strconv.Itoa(t.text.rows + 1)) +} + +func (t *textGridRow) updateGridSize(size fyne.Size) { + bufCols := int(size.Width / t.text.cellSize.Width) + for _, row := range t.text.text.Rows { + lenCells := len(row.Cells) + if lenCells > bufCols { + bufCols = lenCells + } + } + + if t.text.text.ShowWhitespace { + bufCols++ + } + if t.text.text.ShowLineNumbers { + bufCols += t.lineNumberWidth() + } + + t.cols = bufCols + t.addCellsIfRequired() +} + +type textGridRowRenderer struct { + obj *textGridRow +} + +func (t *textGridRowRenderer) Layout(size fyne.Size) { + t.obj.updateGridSize(size) + + cellPos := fyne.NewPos(0, 0) + off := 0 + for x := 0; x < t.obj.cols; x++ { + // rect + t.obj.objects[off].Resize(t.obj.text.cellSize) + t.obj.objects[off].Move(cellPos) + + // text + t.obj.objects[off+1].Move(cellPos) + + // underline + t.obj.objects[off+2].Move(cellPos.Add(fyne.Position{X: 0, Y: t.obj.text.cellSize.Height})) + t.obj.objects[off+2].Resize(fyne.Size{Width: t.obj.text.cellSize.Width}) + + cellPos.X += t.obj.text.cellSize.Width + off += 3 + } +} + +func (t *textGridRowRenderer) MinSize() fyne.Size { + longestRow := float32(0) + for _, row := range t.obj.text.text.Rows { + longestRow = fyne.Max(longestRow, float32(len(row.Cells))) + } + return fyne.NewSize(t.obj.text.cellSize.Width*longestRow, t.obj.text.cellSize.Height) +} + +func (t *textGridRowRenderer) Refresh() { + th := t.obj.text.text.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + t.obj.cachedFGColor = th.Color(theme.ColorNameForeground, v) + t.obj.cachedTextSize = th.Size(theme.SizeNameText) + TextGridStyleWhitespace = &CustomTextGridStyle{FGColor: th.Color(theme.ColorNameDisabled, v)} + t.obj.updateGridSize(t.obj.text.text.Size()) + t.obj.refreshCells() +} + +func (t *textGridRowRenderer) ApplyTheme() { +} + +func (t *textGridRowRenderer) Objects() []fyne.CanvasObject { + return t.obj.objects +} + +func (t *textGridRowRenderer) Destroy() { +} diff --git a/vendor/fyne.io/fyne/v2/widget/toolbar.go b/vendor/fyne.io/fyne/v2/widget/toolbar.go new file mode 100644 index 0000000..77c4512 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/toolbar.go @@ -0,0 +1,163 @@ +package widget + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/internal/widget" + "fyne.io/fyne/v2/layout" +) + +// ToolbarItem represents any interface element that can be added to a toolbar +type ToolbarItem interface { + ToolbarObject() fyne.CanvasObject +} + +// ToolbarAction is push button style of ToolbarItem +type ToolbarAction struct { + Icon fyne.Resource + OnActivated func() `json:"-"` + button Button +} + +// ToolbarObject gets a button to render this ToolbarAction +func (t *ToolbarAction) ToolbarObject() fyne.CanvasObject { + t.button.Importance = LowImportance + + // synchronize properties + t.button.Icon = t.Icon + t.button.OnTapped = t.OnActivated + + return &t.button +} + +// SetIcon updates the icon on a ToolbarItem +// +// Since: 2.2 +func (t *ToolbarAction) SetIcon(icon fyne.Resource) { + t.Icon = icon + t.button.SetIcon(t.Icon) +} + +// Enable this ToolbarAction, updating any style or features appropriately. +// +// Since: 2.5 +func (t *ToolbarAction) Enable() { + t.button.Enable() +} + +// Disable this ToolbarAction so that it cannot be interacted with, updating any style appropriately. +// +// Since: 2.5 +func (t *ToolbarAction) Disable() { + t.button.Disable() +} + +// Disabled returns true if this ToolbarAction is currently disabled or false if it can currently be interacted with. +// +// Since: 2.5 +func (t *ToolbarAction) Disabled() bool { + return t.button.Disabled() +} + +// NewToolbarAction returns a new push button style ToolbarItem +func NewToolbarAction(icon fyne.Resource, onActivated func()) *ToolbarAction { + return &ToolbarAction{Icon: icon, OnActivated: onActivated} +} + +// ToolbarSpacer is a blank, stretchable space for a toolbar. +// This is typically used to assist layout if you wish some left and some right aligned items. +// Space will be split evebly amongst all the spacers on a toolbar. +type ToolbarSpacer struct{} + +// ToolbarObject gets the actual spacer object for this ToolbarSpacer +func (t *ToolbarSpacer) ToolbarObject() fyne.CanvasObject { + return layout.NewSpacer() +} + +// NewToolbarSpacer returns a new spacer item for a Toolbar to assist with ToolbarItem alignment +func NewToolbarSpacer() *ToolbarSpacer { + return &ToolbarSpacer{} +} + +// ToolbarSeparator is a thin, visible divide that can be added to a Toolbar. +// This is typically used to assist visual grouping of ToolbarItems. +type ToolbarSeparator struct{} + +// ToolbarObject gets the visible line object for this ToolbarSeparator +func (t *ToolbarSeparator) ToolbarObject() fyne.CanvasObject { + return &Separator{invert: true} +} + +// NewToolbarSeparator returns a new separator item for a Toolbar to assist with ToolbarItem grouping +func NewToolbarSeparator() *ToolbarSeparator { + return &ToolbarSeparator{} +} + +// Toolbar widget creates a horizontal list of tool buttons +type Toolbar struct { + BaseWidget + Items []ToolbarItem +} + +// CreateRenderer is a private method to Fyne which links this widget to its renderer +func (t *Toolbar) CreateRenderer() fyne.WidgetRenderer { + t.ExtendBaseWidget(t) + r := &toolbarRenderer{toolbar: t, layout: layout.NewHBoxLayout()} + r.resetObjects() + return r +} + +// Append a new ToolbarItem to the end of this Toolbar +func (t *Toolbar) Append(item ToolbarItem) { + t.Items = append(t.Items, item) + t.Refresh() +} + +// Prepend a new ToolbarItem to the start of this Toolbar +func (t *Toolbar) Prepend(item ToolbarItem) { + t.Items = append([]ToolbarItem{item}, t.Items...) + t.Refresh() +} + +// MinSize returns the size that this widget should not shrink below +func (t *Toolbar) MinSize() fyne.Size { + t.ExtendBaseWidget(t) + return t.BaseWidget.MinSize() +} + +// NewToolbar creates a new toolbar widget. +func NewToolbar(items ...ToolbarItem) *Toolbar { + t := &Toolbar{Items: items} + t.ExtendBaseWidget(t) + + t.Refresh() + return t +} + +type toolbarRenderer struct { + widget.BaseRenderer + layout fyne.Layout + items []fyne.CanvasObject + toolbar *Toolbar +} + +func (r *toolbarRenderer) MinSize() fyne.Size { + return r.layout.MinSize(r.items) +} + +func (r *toolbarRenderer) Layout(size fyne.Size) { + r.layout.Layout(r.items, size) +} + +func (r *toolbarRenderer) Refresh() { + r.resetObjects() + canvas.Refresh(r.toolbar) +} + +func (r *toolbarRenderer) resetObjects() { + r.items = make([]fyne.CanvasObject, 0, len(r.toolbar.Items)) + for _, item := range r.toolbar.Items { + r.items = append(r.items, item.ToolbarObject()) + } + r.SetObjects(r.items) +} diff --git a/vendor/fyne.io/fyne/v2/widget/tree.go b/vendor/fyne.io/fyne/v2/widget/tree.go new file mode 100644 index 0000000..9ce78b8 --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/tree.go @@ -0,0 +1,1079 @@ +package widget + +import ( + "fmt" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/data/binding" + "fyne.io/fyne/v2/driver/desktop" + "fyne.io/fyne/v2/internal/async" + "fyne.io/fyne/v2/internal/cache" + "fyne.io/fyne/v2/internal/widget" + "fyne.io/fyne/v2/theme" +) + +// TreeNodeID represents the unique id of a tree node. +type TreeNodeID = string + +const ( + // allTreeNodesID represents all tree nodes when refreshing requested nodes + allTreeNodesID TreeNodeID = "_ALLNODES" + onlyNewTreeNodesID TreeNodeID = "_ONLYNEWNODES" +) + +// Declare conformity with interfaces +var ( + _ fyne.Focusable = (*Tree)(nil) + _ fyne.Widget = (*Tree)(nil) +) + +// Tree widget displays hierarchical data. +// Each node of the tree must be identified by a Unique TreeNodeID. +// +// Since: 1.4 +type Tree struct { + BaseWidget + Root TreeNodeID + + // HideSeparators hides the separators between tree nodes + // + // Since: 2.5 + HideSeparators bool + + ChildUIDs func(uid TreeNodeID) (c []TreeNodeID) `json:"-"` // Return a sorted slice of Children TreeNodeIDs for the given Node TreeNodeID + CreateNode func(branch bool) (o fyne.CanvasObject) `json:"-"` // Return a CanvasObject that can represent a Branch (if branch is true), or a Leaf (if branch is false) + IsBranch func(uid TreeNodeID) (ok bool) `json:"-"` // Return true if the given TreeNodeID represents a Branch + OnBranchClosed func(uid TreeNodeID) `json:"-"` // Called when a Branch is closed + OnBranchOpened func(uid TreeNodeID) `json:"-"` // Called when a Branch is opened + OnSelected func(uid TreeNodeID) `json:"-"` // Called when the Node with the given TreeNodeID is selected. + OnUnselected func(uid TreeNodeID) `json:"-"` // Called when the Node with the given TreeNodeID is unselected. + UpdateNode func(uid TreeNodeID, branch bool, node fyne.CanvasObject) `json:"-"` // Called to update the given CanvasObject to represent the data at the given TreeNodeID + + branchMinSize fyne.Size + currentFocus TreeNodeID + focused bool + leafMinSize fyne.Size + offset fyne.Position + open map[TreeNodeID]bool + scroller *widget.Scroll + selected []TreeNodeID +} + +// NewTree returns a new performant tree widget defined by the passed functions. +// childUIDs returns the child TreeNodeIDs of the given node. +// isBranch returns true if the given node is a branch, false if it is a leaf. +// create returns a new template object that can be cached. +// update is used to apply data at specified data location to the passed template CanvasObject. +// +// Since: 1.4 +func NewTree(childUIDs func(TreeNodeID) []TreeNodeID, isBranch func(TreeNodeID) bool, create func(bool) fyne.CanvasObject, update func(TreeNodeID, bool, fyne.CanvasObject)) *Tree { + t := &Tree{ChildUIDs: childUIDs, IsBranch: isBranch, CreateNode: create, UpdateNode: update} + t.ExtendBaseWidget(t) + return t +} + +// NewTreeWithData creates a new tree widget that will display the contents of the provided data. +// +// Since: 2.4 +func NewTreeWithData(data binding.DataTree, createItem func(bool) fyne.CanvasObject, updateItem func(binding.DataItem, bool, fyne.CanvasObject)) *Tree { + t := NewTree( + data.ChildIDs, + func(id TreeNodeID) bool { + children := data.ChildIDs(id) + return len(children) > 0 + }, + createItem, + func(i TreeNodeID, branch bool, o fyne.CanvasObject) { + item, err := data.GetItem(i) + if err != nil { + fyne.LogError(fmt.Sprintf("Error getting data item %s", i), err) + return + } + updateItem(item, branch, o) + }) + + data.AddListener(binding.NewDataListener(t.Refresh)) + return t +} + +// NewTreeWithStrings creates a new tree with the given string map. +// Data must contain a mapping for the root, which defaults to empty string (""). +// +// Since: 1.4 +func NewTreeWithStrings(data map[string][]string) (t *Tree) { + t = &Tree{ + ChildUIDs: func(uid string) (c []string) { + c = data[uid] + return c + }, + IsBranch: func(uid string) (b bool) { + _, b = data[uid] + return b + }, + CreateNode: func(branch bool) fyne.CanvasObject { + return NewLabel("Template Object") + }, + UpdateNode: func(uid string, branch bool, node fyne.CanvasObject) { + node.(*Label).SetText(uid) + }, + } + t.ExtendBaseWidget(t) + return t +} + +// CloseAllBranches closes all branches in the tree. +func (t *Tree) CloseAllBranches() { + t.open = make(map[TreeNodeID]bool) + t.Refresh() +} + +// CloseBranch closes the branch with the given TreeNodeID. +func (t *Tree) CloseBranch(uid TreeNodeID) { + t.ensureOpenMap() + t.open[uid] = false + if f := t.OnBranchClosed; f != nil { + f(uid) + } + t.Refresh() +} + +// CreateRenderer is a private method to Fyne which links this widget to its renderer. +func (t *Tree) CreateRenderer() fyne.WidgetRenderer { + t.ExtendBaseWidget(t) + c := newTreeContent(t) + s := widget.NewScroll(c) + t.scroller = s + r := &treeRenderer{ + BaseRenderer: widget.NewBaseRenderer([]fyne.CanvasObject{s}), + tree: t, + content: c, + scroller: s, + } + s.OnScrolled = t.offsetUpdated + r.updateMinSizes() + r.content.viewport = r.MinSize() + return r +} + +// IsBranchOpen returns true if the branch with the given TreeNodeID is expanded. +func (t *Tree) IsBranchOpen(uid TreeNodeID) bool { + if uid == t.Root { + return true // Root is always open + } + t.ensureOpenMap() + return t.open[uid] +} + +// FocusGained is called after this Tree has gained focus. +func (t *Tree) FocusGained() { + if t.currentFocus == "" { + if childUIDs := t.ChildUIDs; childUIDs != nil { + if ids := childUIDs(""); len(ids) > 0 { + t.setItemFocus(ids[0]) + } + } + } + + t.focused = true + t.RefreshItem(t.currentFocus) +} + +// FocusLost is called after this Tree has lost focus. +func (t *Tree) FocusLost() { + t.focused = false + t.Refresh() // Item(t.currentFocus) +} + +// MinSize returns the size that this widget should not shrink below. +func (t *Tree) MinSize() fyne.Size { + t.ExtendBaseWidget(t) + return t.BaseWidget.MinSize() +} + +// RefreshItem refreshes a single item, specified by the item ID passed in. +// +// Since: 2.4 +func (t *Tree) RefreshItem(id TreeNodeID) { + if t.scroller == nil { + return + } + t.scroller.Content.(*treeContent).refreshForID(id) +} + +// OpenAllBranches opens all branches in the tree. +func (t *Tree) OpenAllBranches() { + t.ensureOpenMap() + t.walkAll(func(uid, parent TreeNodeID, branch bool, depth int) { + if branch { + t.open[uid] = true + } + }) + t.Refresh() +} + +// OpenBranch opens the branch with the given TreeNodeID. +func (t *Tree) OpenBranch(uid TreeNodeID) { + t.ensureOpenMap() + t.open[uid] = true + if f := t.OnBranchOpened; f != nil { + f(uid) + } + t.Refresh() +} + +// Resize sets a new size for a widget. +func (t *Tree) Resize(size fyne.Size) { + if size == t.Size() { + return + } + t.BaseWidget.Resize(size) + if t.scroller == nil { + return + } + t.scroller.Content.(*treeContent).refreshForID(onlyNewTreeNodesID) +} + +// ScrollToBottom scrolls to the bottom of the tree. +// +// Since 2.1 +func (t *Tree) ScrollToBottom() { + if t.scroller == nil { + return + } + + t.scroller.ScrollToBottom() + t.offsetUpdated(t.scroller.Offset) +} + +// ScrollTo scrolls to the node with the given id. +// +// Since 2.1 +func (t *Tree) ScrollTo(uid TreeNodeID) { + if t.scroller == nil { + return + } + + y, size, ok := t.offsetAndSize(uid) + if !ok { + return + } + + // TODO scrolling to a node should open all parents if they aren't already + newY := t.scroller.Offset.Y + if y < t.scroller.Offset.Y { + newY = y + } else if y+size.Height > t.scroller.Offset.Y+t.scroller.Size().Height { + newY = y + size.Height - t.scroller.Size().Height + } + + t.scroller.ScrollToOffset(fyne.NewPos(t.scroller.Offset.X, newY)) + t.offsetUpdated(t.scroller.Offset) +} + +// ScrollToOffset scrolls the tree to the given offset position. +// +// Since: 2.6 +func (t *Tree) ScrollToOffset(offset float32) { + if t.scroller == nil { + return + } + if offset < 0 { + offset = 0 + } + + t.scroller.ScrollToOffset(fyne.NewPos(t.scroller.Offset.X, offset)) + t.offsetUpdated(t.scroller.Offset) +} + +// ScrollToTop scrolls to the top of the tree. +// +// Since 2.1 +func (t *Tree) ScrollToTop() { + if t.scroller == nil { + return + } + + t.scroller.ScrollToTop() + t.offsetUpdated(t.scroller.Offset) +} + +// Select marks the specified node to be selected. +func (t *Tree) Select(uid TreeNodeID) { + t.setItemFocus(uid) + if len(t.selected) > 0 { + if uid == t.selected[0] { + return // no change + } + if f := t.OnUnselected; f != nil { + f(t.selected[0]) + } + } + t.selected = []TreeNodeID{uid} + t.Refresh() + t.ScrollTo(uid) + if f := t.OnSelected; f != nil { + f(uid) + } +} + +func (t *Tree) setItemFocus(uid TreeNodeID) { + if t.currentFocus == uid { + return + } + + previous := t.currentFocus + t.currentFocus = uid + t.RefreshItem(previous) + t.ScrollTo(t.currentFocus) + t.RefreshItem(t.currentFocus) +} + +// ToggleBranch flips the state of the branch with the given TreeNodeID. +func (t *Tree) ToggleBranch(uid string) { + if t.IsBranchOpen(uid) { + t.CloseBranch(uid) + } else { + t.OpenBranch(uid) + } +} + +// TypedKey is called if a key event happens while this Tree is focused. +func (t *Tree) TypedKey(event *fyne.KeyEvent) { + switch event.Name { + case fyne.KeySpace: + t.Select(t.currentFocus) + case fyne.KeyDown: + next := false + t.walk(t.Root, "", 0, func(id, p TreeNodeID, _ bool, _ int) { + if next { + t.setItemFocus(id) + next = false + } else if id == t.currentFocus { + next = true + } + }) + case fyne.KeyLeft: + // If the current focus is on a branch which is open, just close it + if t.IsBranch(t.currentFocus) && t.IsBranchOpen(t.currentFocus) { + t.CloseBranch(t.currentFocus) + } else { + // Every other case should move the focus to the current parent node + t.walk(t.Root, "", 0, func(id, p TreeNodeID, _ bool, _ int) { + if id == t.currentFocus && p != "" { + t.setItemFocus(p) + } + }) + } + case fyne.KeyRight: + if t.IsBranch(t.currentFocus) { + t.OpenBranch(t.currentFocus) + } + children := []TreeNodeID{} + if childUIDs := t.ChildUIDs; childUIDs != nil { + children = childUIDs(t.currentFocus) + } + + if len(children) > 0 { + t.setItemFocus(children[0]) + } + case fyne.KeyUp: + previous := "" + t.walk(t.Root, "", 0, func(id, p TreeNodeID, _ bool, _ int) { + if id == t.currentFocus && previous != "" { + t.setItemFocus(previous) + } + previous = id + }) + } +} + +// TypedRune is called if a text event happens while this Tree is focused. +func (t *Tree) TypedRune(_ rune) { + // intentionally left blank +} + +// Unselect marks the specified node to be not selected. +func (t *Tree) Unselect(uid TreeNodeID) { + if len(t.selected) == 0 || t.selected[0] != uid { + return + } + + t.selected = nil + t.Refresh() + if f := t.OnUnselected; f != nil { + f(uid) + } +} + +// UnselectAll sets all nodes to be not selected. +// +// Since: 2.1 +func (t *Tree) UnselectAll() { + if len(t.selected) == 0 { + return + } + + selected := t.selected + t.selected = nil + t.Refresh() + if f := t.OnUnselected; f != nil { + for _, uid := range selected { + f(uid) + } + } +} + +func (t *Tree) ensureOpenMap() { + if t.open == nil { + t.open = make(map[string]bool) + } +} + +func (t *Tree) offsetAndSize(uid TreeNodeID) (y float32, size fyne.Size, found bool) { + pad := t.Theme().Size(theme.SizeNamePadding) + + t.walkAll(func(id, _ TreeNodeID, branch bool, _ int) { + m := t.leafMinSize + if branch { + m = t.branchMinSize + } + if id == uid { + found = true + size = m + } else if !found { + // Root node is not rendered unless it has been customized + if t.Root == "" && id == "" { + // This is root node, skip + return + } + // If this is not the first item, add a separator + if y > 0 { + y += pad + } + + y += m.Height + } + }) + return y, size, found +} + +func (t *Tree) offsetUpdated(pos fyne.Position) { + if t.offset == pos { + return + } + t.offset = pos + t.scroller.Content.(*treeContent).refreshForID(onlyNewTreeNodesID) +} + +func (t *Tree) walk(uid, parent TreeNodeID, depth int, onNode func(TreeNodeID, TreeNodeID, bool, int)) { + if isBranch := t.IsBranch; isBranch != nil { + if isBranch(uid) { + onNode(uid, parent, true, depth) + if t.IsBranchOpen(uid) { + if childUIDs := t.ChildUIDs; childUIDs != nil { + for _, c := range childUIDs(uid) { + t.walk(c, uid, depth+1, onNode) + } + } + } + } else { + onNode(uid, parent, false, depth) + } + } +} + +// walkAll visits every open node of the tree and calls the given callback with TreeNodeID, whether node is branch, and the depth of node. +func (t *Tree) walkAll(onNode func(TreeNodeID, TreeNodeID, bool, int)) { + t.walk(t.Root, "", 0, onNode) +} + +var _ fyne.WidgetRenderer = (*treeRenderer)(nil) + +type treeRenderer struct { + widget.BaseRenderer + tree *Tree + content *treeContent + scroller *widget.Scroll +} + +func (r *treeRenderer) MinSize() (min fyne.Size) { + min = r.scroller.MinSize() + min = min.Max(r.tree.branchMinSize) + min = min.Max(r.tree.leafMinSize) + return min +} + +func (r *treeRenderer) Layout(size fyne.Size) { + r.content.viewport = size + r.scroller.Resize(size) + r.tree.offsetUpdated(r.scroller.Offset) +} + +func (r *treeRenderer) Refresh() { + r.updateMinSizes() + s := r.tree.Size() + if s.IsZero() { + r.tree.Resize(r.tree.MinSize()) + } else { + r.Layout(s) + } + r.scroller.Refresh() + r.content.Refresh() + canvas.Refresh(r.tree.super()) +} + +func (r *treeRenderer) updateMinSizes() { + if f := r.tree.CreateNode; f != nil { + branch := createItemAndApplyThemeScope(func() fyne.CanvasObject { return f(true) }, r.tree) + r.tree.branchMinSize = newBranch(r.tree, branch).MinSize() + + leaf := createItemAndApplyThemeScope(func() fyne.CanvasObject { return f(false) }, r.tree) + r.tree.leafMinSize = newLeaf(r.tree, leaf).MinSize() + } +} + +var _ fyne.Widget = (*treeContent)(nil) + +type treeContent struct { + BaseWidget + tree *Tree + viewport fyne.Size + + nextRefreshID TreeNodeID +} + +func newTreeContent(tree *Tree) (c *treeContent) { + c = &treeContent{ + tree: tree, + } + c.ExtendBaseWidget(c) + return c +} + +func (c *treeContent) CreateRenderer() fyne.WidgetRenderer { + return &treeContentRenderer{ + BaseRenderer: widget.BaseRenderer{}, + treeContent: c, + branches: make(map[string]*branch), + leaves: make(map[string]*leaf), + } +} + +func (c *treeContent) Resize(size fyne.Size) { + if size == c.Size() { + return + } + + c.size = size + + c.Refresh() // trigger a redraw +} + +func (c *treeContent) refreshForID(id TreeNodeID) { + c.nextRefreshID = id + c.BaseWidget.Refresh() +} + +func (c *treeContent) Refresh() { + c.nextRefreshID = allTreeNodesID + c.BaseWidget.Refresh() +} + +var _ fyne.WidgetRenderer = (*treeContentRenderer)(nil) + +type treeContentRenderer struct { + widget.BaseRenderer + treeContent *treeContent + separators []fyne.CanvasObject + objects []fyne.CanvasObject + branches map[string]*branch + leaves map[string]*leaf + branchPool async.Pool[fyne.CanvasObject] + leafPool async.Pool[fyne.CanvasObject] + + wasVisible []TreeNodeID + visible []TreeNodeID +} + +func (r *treeContentRenderer) Layout(size fyne.Size) { + th := r.treeContent.Theme() + r.objects = nil + branches := make(map[string]*branch) + leaves := make(map[string]*leaf) + + pad := th.Size(theme.SizeNamePadding) + offsetY := r.treeContent.tree.offset.Y + viewport := r.treeContent.viewport + width := fyne.Max(size.Width, viewport.Width) + separatorCount := 0 + separatorThickness := th.Size(theme.SizeNameSeparatorThickness) + separatorSize := fyne.NewSize(width, separatorThickness) + separatorOff := (pad + separatorThickness) / 2 + hideSeparators := r.treeContent.tree.HideSeparators + y := float32(0) + + r.wasVisible, r.visible = r.visible, r.wasVisible + r.visible = r.visible[:0] + + // walkAll open branches and obtain nodes to render in scroller's viewport + r.treeContent.tree.walkAll(func(uid, _ string, isBranch bool, depth int) { + // Root node is not rendered unless it has been customized + if r.treeContent.tree.Root == "" { + depth = depth - 1 + if uid == "" { + // This is root node, skip + return + } + } + + // If this is not the first item, add a separator + addSeparator := y > 0 + if addSeparator { + y += pad + separatorCount++ + } + + m := r.treeContent.tree.leafMinSize + if isBranch { + m = r.treeContent.tree.branchMinSize + } + if y+m.Height < offsetY { + // Node is above viewport and not visible + } else if y > offsetY+viewport.Height { + // Node is below viewport and not visible + } else { + // Node is in viewport + r.visible = append(r.visible, uid) + + if addSeparator && !hideSeparators { + var separator fyne.CanvasObject + if separatorCount < len(r.separators) { + separator = r.separators[separatorCount] + separator.Show() // it may previously have been hidden + } else { + separator = NewSeparator() + r.separators = append(r.separators, separator) + } + separator.Move(fyne.NewPos(0, y-separatorOff)) + separator.Resize(separatorSize) + r.objects = append(r.objects, separator) + separatorCount++ + } + + var n fyne.CanvasObject + if isBranch { + b, ok := r.branches[uid] + if !ok { + b = r.getBranch() + if f := r.treeContent.tree.UpdateNode; f != nil { + f(uid, true, b.Content()) + } + b.update(uid, depth) + } + branches[uid] = b + n = b + r.objects = append(r.objects, b) + } else { + l, ok := r.leaves[uid] + if !ok { + l = r.getLeaf() + if f := r.treeContent.tree.UpdateNode; f != nil { + f(uid, false, l.Content()) + } + l.update(uid, depth) + } + leaves[uid] = l + n = l + r.objects = append(r.objects, l) + } + if n != nil { + n.Move(fyne.NewPos(0, y)) + n.Resize(fyne.NewSize(width, m.Height)) + } + } + y += m.Height + }) + + if hideSeparators { + // start below iteration from 0 to hide all separators + separatorCount = 0 + } + // Hide any separators that haven't been reused + for ; separatorCount < len(r.separators); separatorCount++ { + r.separators[separatorCount].Hide() + } + + // Release any nodes that haven't been reused + for uid, b := range r.branches { + if _, ok := branches[uid]; !ok { + r.branchPool.Put(b) + } + } + for uid, l := range r.leaves { + if _, ok := leaves[uid]; !ok { + r.leafPool.Put(l) + } + } + + r.branches = branches + r.leaves = leaves +} + +func (r *treeContentRenderer) MinSize() (min fyne.Size) { + th := r.treeContent.Theme() + pad := th.Size(theme.SizeNamePadding) + iconSize := th.Size(theme.SizeNameInlineIcon) + + r.treeContent.tree.walkAll(func(uid, _ string, isBranch bool, depth int) { + // Root node is not rendered unless it has been customized + if r.treeContent.tree.Root == "" { + depth = depth - 1 + if uid == "" { + // This is root node, skip + return + } + } + + // If this is not the first item, add a separator + if min.Height > 0 { + min.Height += pad + } + + m := r.treeContent.tree.leafMinSize + if isBranch { + m = r.treeContent.tree.branchMinSize + } + m.Width += float32(depth) * (iconSize + pad) + min.Width = fyne.Max(min.Width, m.Width) + min.Height += m.Height + }) + return min +} + +func (r *treeContentRenderer) Objects() []fyne.CanvasObject { + return r.objects +} + +func (r *treeContentRenderer) Refresh() { + r.refreshForID(r.treeContent.nextRefreshID) + for _, s := range r.separators { + s.Refresh() + } +} + +func (r *treeContentRenderer) refreshForID(toDraw TreeNodeID) { + s := r.treeContent.Size() + if s.IsZero() { + r.treeContent.Resize(r.treeContent.MinSize().Max(r.treeContent.tree.Size())) + } else { + r.Layout(s) + } + + if toDraw == onlyNewTreeNodesID { + for id, b := range r.branches { + if contains(r.visible, id) && !contains(r.wasVisible, id) { + b.Refresh() + } + } + return + } + + for id, b := range r.branches { + if toDraw != allTreeNodesID && id != toDraw { + continue + } + + b.Refresh() + } + for id, l := range r.leaves { + if toDraw != allTreeNodesID && id != toDraw { + continue + } + + l.Refresh() + } + canvas.Refresh(r.treeContent.super()) +} + +func (r *treeContentRenderer) getBranch() (b *branch) { + o := r.branchPool.Get() + if o != nil { + b = o.(*branch) + } else { + var content fyne.CanvasObject + if f := r.treeContent.tree.CreateNode; f != nil { + content = createItemAndApplyThemeScope(func() fyne.CanvasObject { return f(true) }, r.treeContent.tree) + } + b = newBranch(r.treeContent.tree, content) + } + return b +} + +func (r *treeContentRenderer) getLeaf() (l *leaf) { + o := r.leafPool.Get() + if o != nil { + l = o.(*leaf) + } else { + var content fyne.CanvasObject + if f := r.treeContent.tree.CreateNode; f != nil { + content = createItemAndApplyThemeScope(func() fyne.CanvasObject { return f(false) }, r.treeContent.tree) + } + l = newLeaf(r.treeContent.tree, content) + } + return l +} + +var ( + _ desktop.Hoverable = (*treeNode)(nil) + _ fyne.CanvasObject = (*treeNode)(nil) + _ fyne.Tappable = (*treeNode)(nil) +) + +type treeNode struct { + BaseWidget + tree *Tree + uid string + depth int + hovered bool + icon fyne.CanvasObject + isBranch bool + content fyne.CanvasObject +} + +func (n *treeNode) Content() fyne.CanvasObject { + return n.content +} + +func (n *treeNode) CreateRenderer() fyne.WidgetRenderer { + th := n.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + background := canvas.NewRectangle(th.Color(theme.ColorNameHover, v)) + background.CornerRadius = th.Size(theme.SizeNameSelectionRadius) + background.Hide() + return &treeNodeRenderer{ + BaseRenderer: widget.BaseRenderer{}, + treeNode: n, + background: background, + } +} + +func (n *treeNode) Indent() float32 { + th := n.Theme() + return float32(n.depth) * (th.Size(theme.SizeNameInlineIcon) + th.Size(theme.SizeNamePadding)) +} + +// MouseIn is called when a desktop pointer enters the widget +func (n *treeNode) MouseIn(*desktop.MouseEvent) { + n.hovered = true + n.partialRefresh() +} + +// MouseMoved is called when a desktop pointer hovers over the widget +func (n *treeNode) MouseMoved(*desktop.MouseEvent) { +} + +// MouseOut is called when a desktop pointer exits the widget +func (n *treeNode) MouseOut() { + n.hovered = false + n.partialRefresh() +} + +func (n *treeNode) Tapped(*fyne.PointEvent) { + n.tree.Select(n.uid) + canvas := fyne.CurrentApp().Driver().CanvasForObject(n.tree) + if canvas != nil && canvas.Focused() != n.tree { + if !fyne.CurrentDevice().IsMobile() { + canvas.Focus(n.tree.impl.(fyne.Focusable)) + } + } + n.Refresh() +} + +func (n *treeNode) partialRefresh() { + if r := cache.Renderer(n.super()); r != nil { + r.(*treeNodeRenderer).partialRefresh() + } +} + +func (n *treeNode) update(uid string, depth int) { + n.uid = uid + n.depth = depth + n.Hidden = false + n.partialRefresh() +} + +var _ fyne.WidgetRenderer = (*treeNodeRenderer)(nil) + +type treeNodeRenderer struct { + widget.BaseRenderer + treeNode *treeNode + background *canvas.Rectangle +} + +func (r *treeNodeRenderer) Layout(size fyne.Size) { + th := r.treeNode.Theme() + pad := th.Size(theme.SizeNamePadding) + iconSize := th.Size(theme.SizeNameInlineIcon) + x := pad + r.treeNode.Indent() + y := float32(0) + r.background.Resize(size) + if r.treeNode.icon != nil { + r.treeNode.icon.Move(fyne.NewPos(x, y)) + r.treeNode.icon.Resize(fyne.NewSize(iconSize, size.Height)) + } + x += iconSize + x += pad + if r.treeNode.content != nil { + r.treeNode.content.Move(fyne.NewPos(x, y)) + r.treeNode.content.Resize(fyne.NewSize(size.Width-x, size.Height)) + } +} + +func (r *treeNodeRenderer) MinSize() (min fyne.Size) { + if r.treeNode.content != nil { + min = r.treeNode.content.MinSize() + } + th := r.treeNode.Theme() + iconSize := th.Size(theme.SizeNameInlineIcon) + + min.Width += th.Size(theme.SizeNameInnerPadding) + r.treeNode.Indent() + iconSize + min.Height = fyne.Max(min.Height, iconSize) + return min +} + +func (r *treeNodeRenderer) Objects() (objects []fyne.CanvasObject) { + objects = append(objects, r.background) + if r.treeNode.content != nil { + objects = append(objects, r.treeNode.content) + } + if r.treeNode.icon != nil { + objects = append(objects, r.treeNode.icon) + } + return objects +} + +func (r *treeNodeRenderer) Refresh() { + if c := r.treeNode.content; c != nil { + if f := r.treeNode.tree.UpdateNode; f != nil { + f(r.treeNode.uid, r.treeNode.isBranch, c) + } + } + r.partialRefresh() +} + +func (r *treeNodeRenderer) partialRefresh() { + th := r.treeNode.Theme() + v := fyne.CurrentApp().Settings().ThemeVariant() + + if r.treeNode.icon != nil { + r.treeNode.icon.Refresh() + } + r.background.CornerRadius = th.Size(theme.SizeNameSelectionRadius) + if len(r.treeNode.tree.selected) > 0 && r.treeNode.uid == r.treeNode.tree.selected[0] { + r.background.FillColor = th.Color(theme.ColorNameSelection, v) + r.background.Show() + } else if r.treeNode.hovered || (r.treeNode.tree.focused && r.treeNode.tree.currentFocus == r.treeNode.uid) { + r.background.FillColor = th.Color(theme.ColorNameHover, v) + r.background.Show() + } else { + r.background.Hide() + } + r.background.Refresh() + r.Layout(r.treeNode.Size()) + canvas.Refresh(r.treeNode.super()) +} + +var _ fyne.Widget = (*branch)(nil) + +type branch struct { + *treeNode +} + +func newBranch(tree *Tree, content fyne.CanvasObject) (b *branch) { + b = &branch{ + treeNode: &treeNode{ + tree: tree, + icon: newBranchIcon(tree), + isBranch: true, + content: content, + }, + } + b.ExtendBaseWidget(b) + + if cache.OverrideThemeMatchingScope(b, tree) { + b.Refresh() + } + return b +} + +func (b *branch) update(uid string, depth int) { + b.treeNode.update(uid, depth) + b.icon.(*branchIcon).update(uid) +} + +var _ fyne.Tappable = (*branchIcon)(nil) + +type branchIcon struct { + Icon + tree *Tree + uid string +} + +func newBranchIcon(tree *Tree) (i *branchIcon) { + i = &branchIcon{ + tree: tree, + } + i.ExtendBaseWidget(i) + return i +} + +func (i *branchIcon) Refresh() { + if i.tree.IsBranchOpen(i.uid) { + i.Resource = theme.MoveDownIcon() + } else { + i.Resource = theme.NavigateNextIcon() + } + i.Icon.Refresh() +} + +func (i *branchIcon) Tapped(*fyne.PointEvent) { + i.tree.ToggleBranch(i.uid) +} + +func (i *branchIcon) update(uid string) { + i.uid = uid + i.Refresh() +} + +var _ fyne.Widget = (*leaf)(nil) + +type leaf struct { + *treeNode +} + +func newLeaf(tree *Tree, content fyne.CanvasObject) (l *leaf) { + l = &leaf{ + &treeNode{ + tree: tree, + content: content, + isBranch: false, + }, + } + l.ExtendBaseWidget(l) + + if cache.OverrideThemeMatchingScope(l, tree) { + l.Refresh() + } + return l +} + +func contains(slice []string, item string) bool { + for _, s := range slice { + if s == item { + return true + } + } + return false +} diff --git a/vendor/fyne.io/fyne/v2/widget/widget.go b/vendor/fyne.io/fyne/v2/widget/widget.go new file mode 100644 index 0000000..81fa08a --- /dev/null +++ b/vendor/fyne.io/fyne/v2/widget/widget.go @@ -0,0 +1,226 @@ +// Package widget defines the UI widgets within the Fyne toolkit. +package widget // import "fyne.io/fyne/v2/widget" + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/internal/cache" + internalWidget "fyne.io/fyne/v2/internal/widget" + "fyne.io/fyne/v2/theme" +) + +// BaseWidget provides a helper that handles basic widget behaviours. +type BaseWidget struct { + noCopy noCopy // so `go vet` can complain if a widget is passed by value (copied) + + size fyne.Size + position fyne.Position + Hidden bool + + impl fyne.Widget + themeCache fyne.Theme +} + +// ExtendBaseWidget is used by an extending widget to make use of BaseWidget functionality. +func (w *BaseWidget) ExtendBaseWidget(wid fyne.Widget) { + if w.super() != nil { + return + } + + w.impl = wid +} + +// Size gets the current size of this widget. +func (w *BaseWidget) Size() fyne.Size { + return w.size +} + +// Resize sets a new size for a widget. +// Note this should not be used if the widget is being managed by a Layout within a Container. +func (w *BaseWidget) Resize(size fyne.Size) { + if size == w.Size() { + return + } + + w.size = size + + impl := w.super() + if impl == nil { + return + } + cache.Renderer(impl).Layout(size) +} + +// Position gets the current position of this widget, relative to its parent. +func (w *BaseWidget) Position() fyne.Position { + return w.position +} + +// Move the widget to a new position, relative to its parent. +// Note this should not be used if the widget is being managed by a Layout within a Container. +func (w *BaseWidget) Move(pos fyne.Position) { + if w.Position() == pos { + return + } + + w.position = pos + internalWidget.Repaint(w.super()) +} + +// MinSize for the widget - it should never be resized below this value. +func (w *BaseWidget) MinSize() fyne.Size { + impl := w.super() + + r := cache.Renderer(impl) + if r == nil { + return fyne.Size{} + } + + return r.MinSize() +} + +// Visible returns whether or not this widget should be visible. +// Note that this may not mean it is currently visible if a parent has been hidden. +func (w *BaseWidget) Visible() bool { + return !w.Hidden +} + +// Show this widget so it becomes visible +func (w *BaseWidget) Show() { + if w.Visible() { + return + } + + w.Hidden = false + + impl := w.super() + if impl == nil { + return + } + impl.Refresh() +} + +// Hide this widget so it is no longer visible +func (w *BaseWidget) Hide() { + if !w.Visible() { + return + } + + w.Hidden = true + + impl := w.super() + if impl == nil { + return + } + canvas.Refresh(impl) +} + +// Refresh causes this widget to be redrawn in its current state +func (w *BaseWidget) Refresh() { + impl := w.super() + if impl == nil { + return + } + + w.themeCache = nil + + cache.Renderer(impl).Refresh() +} + +// Theme returns a cached Theme instance for this widget (or its extending widget). +// This will be the app theme in most cases, or a widget specific theme if it is inside a ThemeOverride container. +// +// Since: 2.5 +func (w *BaseWidget) Theme() fyne.Theme { + cached := w.themeCache + if cached != nil { + return cached + } + + cached = cache.WidgetTheme(w.super()) + // don't cache the default as it may change + if cached == nil { + return theme.Current() + } + + w.themeCache = cached + return cached +} + +// super will return the actual object that this represents. +// If extended then this is the extending widget, otherwise it is nil. +func (w *BaseWidget) super() fyne.Widget { + return w.impl +} + +// DisableableWidget describes an extension to BaseWidget which can be disabled. +// Disabled widgets should have a visually distinct style when disabled, normally using theme.DisabledButtonColor. +type DisableableWidget struct { + BaseWidget + + disabled bool +} + +// Enable this widget, updating any style or features appropriately. +func (w *DisableableWidget) Enable() { + if !w.Disabled() { + return // Enabled already + } + + w.disabled = false + + impl := w.super() + if impl == nil { + return + } + impl.Refresh() +} + +// Disable this widget so that it cannot be interacted with, updating any style appropriately. +func (w *DisableableWidget) Disable() { + if w.Disabled() { + return // Disabled already + } + + w.disabled = true + + impl := w.super() + if impl == nil { + return + } + impl.Refresh() +} + +// Disabled returns true if this widget is currently disabled or false if it can currently be interacted with. +func (w *DisableableWidget) Disabled() bool { + return w.disabled +} + +// NewSimpleRenderer creates a new SimpleRenderer to render a widget using a +// single fyne.CanvasObject. +// +// Since: 2.1 +func NewSimpleRenderer(object fyne.CanvasObject) fyne.WidgetRenderer { + return internalWidget.NewSimpleRenderer(object) +} + +// Orientation controls the horizontal/vertical layout of a widget +type Orientation int + +// Orientation constants to control widget layout +const ( + Horizontal Orientation = 0 + Vertical Orientation = 1 + + // Adaptive will switch between horizontal and vertical layouts according to device orientation. + // This orientation is not always supported and interpretation can vary per-widget. + // + // Since: 2.5 + Adaptive Orientation = 2 +) + +type noCopy struct{} + +func (*noCopy) Lock() {} + +func (*noCopy) Unlock() {} diff --git a/vendor/fyne.io/fyne/v2/window.go b/vendor/fyne.io/fyne/v2/window.go new file mode 100644 index 0000000..bc7492c --- /dev/null +++ b/vendor/fyne.io/fyne/v2/window.go @@ -0,0 +1,107 @@ +package fyne + +// Window describes a user interface window. Depending on the platform an app +// may have many windows or just the one. +type Window interface { + // Title returns the current window title. + // This is typically displayed in the window decorations. + Title() string + // SetTitle updates the current title of the window. + SetTitle(string) + + // FullScreen returns whether or not this window is currently full screen. + FullScreen() bool + // SetFullScreen changes the requested fullScreen property + // true for a fullScreen window and false to unset this. + SetFullScreen(bool) + + // Resize this window to the requested content size. + // The result may not be exactly as desired due to various desktop or + // platform constraints. + Resize(Size) + + // RequestFocus attempts to raise and focus this window. + // This should only be called when you are sure the user would want this window + // to steal focus from any current focused window. + RequestFocus() + + // FixedSize returns whether or not this window should disable resizing. + FixedSize() bool + // SetFixedSize sets a hint that states whether the window should be a fixed + // size or allow resizing. + SetFixedSize(bool) + + // CenterOnScreen places a window at the center of the monitor + // the Window object is currently positioned on. + CenterOnScreen() + + // Padded, normally true, states whether the window should have inner + // padding so that components do not touch the window edge. + Padded() bool + // SetPadded allows applications to specify that a window should have + // no inner padding. Useful for fullscreen or graphic based applications. + SetPadded(bool) + + // Icon returns the window icon, this is used in various ways + // depending on operating system. + // Most commonly this is displayed on the window border or task switcher. + Icon() Resource + + // SetIcon sets the icon resource used for this window. + // If none is set should return the application icon. + SetIcon(Resource) + + // SetMaster indicates that closing this window should exit the app + SetMaster() + + // MainMenu gets the content of the window's top level menu. + MainMenu() *MainMenu + + // SetMainMenu adds a top level menu to this window. + // The way this is rendered will depend on the loaded driver. + SetMainMenu(*MainMenu) + + // SetOnClosed sets a function that runs when the window is closed. + SetOnClosed(func()) + + // SetCloseIntercept sets a function that runs instead of closing if defined. + // [Window.Close] should be called explicitly in the interceptor to close the window. + // + // Since: 1.4 + SetCloseIntercept(func()) + + // SetOnDropped allows setting a window-wide callback to receive dropped items. + // The callback function is called with the absolute position of the drop and a + // slice of all of the dropped URIs. + // + // Since 2.4 + SetOnDropped(func(Position, []URI)) + + // Show the window on screen. + Show() + // Hide the window from the user. + // This will not destroy the window or cause the app to exit. + Hide() + // Close the window. + // If it is he "master" window the app will Quit. + // If it is the only open window and no menu is set via [desktop.App] + // SetSystemTrayMenu the app will also Quit. + Close() + + // ShowAndRun is a shortcut to show the window and then run the application. + // This should be called near the end of a main() function as it will block. + ShowAndRun() + + // Content returns the content of this window. + Content() CanvasObject + // SetContent sets the content of this window. + SetContent(CanvasObject) + // Canvas returns the canvas context to render in the window. + // This can be useful to set a key handler for the window, for example. + Canvas() Canvas + + // Clipboard returns the system clipboard + // + // Deprecated: use App.Clipboard() instead. + Clipboard() Clipboard +} diff --git a/vendor/fyne.io/systray/.gitignore b/vendor/fyne.io/systray/.gitignore new file mode 100644 index 0000000..2ff640b --- /dev/null +++ b/vendor/fyne.io/systray/.gitignore @@ -0,0 +1,13 @@ +example/example +webview_example/webview_example +*~ +*.swp +**/*.exe +Release +Debug +*.sdf +dll/systray_unsigned.dll +out.txt +.vs +on_exit*.txt +.vscode \ No newline at end of file diff --git a/vendor/fyne.io/systray/CHANGELOG.md b/vendor/fyne.io/systray/CHANGELOG.md new file mode 100644 index 0000000..58e7fc8 --- /dev/null +++ b/vendor/fyne.io/systray/CHANGELOG.md @@ -0,0 +1,125 @@ +# Changelog + +## [v1.1.0](https://github.com/getlantern/systray/tree/v1.1.0) (2020-11-18) + +[Full Changelog](https://github.com/getlantern/systray/compare/v1.0.5...v1.1.0) + +**Merged pull requests:** + +- Add submenu support for Linux [\#183](https://github.com/getlantern/systray/pull/183) ([fbrinker](https://github.com/fbrinker)) +- Add checkbox support for Linux [\#181](https://github.com/getlantern/systray/pull/181) ([fbrinker](https://github.com/fbrinker)) +- fix SetTitle documentation [\#179](https://github.com/getlantern/systray/pull/179) ([delthas](https://github.com/delthas)) + +## [v1.0.5](https://github.com/getlantern/systray/tree/v1.0.5) (2020-10-19) + +[Full Changelog](https://github.com/getlantern/systray/compare/v1.0.4...v1.0.5) + +**Merged pull requests:** + +- start menu ID with positive, and change the type to uint32 [\#173](https://github.com/getlantern/systray/pull/173) ([joesis](https://github.com/joesis)) +- Allows disabling items in submenu on macOS [\#172](https://github.com/getlantern/systray/pull/172) ([joesis](https://github.com/joesis)) +- Does not use the template icon for regular icons [\#171](https://github.com/getlantern/systray/pull/171) ([sithembiso](https://github.com/sithembiso)) + +## [v1.0.4](https://github.com/getlantern/systray/tree/v1.0.4) (2020-07-21) + +[Full Changelog](https://github.com/getlantern/systray/compare/1.0.3...v1.0.4) + +**Merged pull requests:** + +- protect shared data structures with proper mutexes [\#162](https://github.com/getlantern/systray/pull/162) ([joesis](https://github.com/joesis)) + +## [1.0.3](https://github.com/getlantern/systray/tree/1.0.3) (2020-06-11) + +[Full Changelog](https://github.com/getlantern/systray/compare/v1.0.3...1.0.3) + +## [v1.0.3](https://github.com/getlantern/systray/tree/v1.0.3) (2020-06-11) + +[Full Changelog](https://github.com/getlantern/systray/compare/v1.0.2...v1.0.3) + +**Merged pull requests:** + +- have a workaround to avoid crash on old macOS versions [\#153](https://github.com/getlantern/systray/pull/153) ([joesis](https://github.com/joesis)) +- Fix bug on darwin of setting icon for menu [\#147](https://github.com/getlantern/systray/pull/147) ([mangalaman93](https://github.com/mangalaman93)) + +## [v1.0.2](https://github.com/getlantern/systray/tree/v1.0.2) (2020-05-19) + +[Full Changelog](https://github.com/getlantern/systray/compare/v1.0.1...v1.0.2) + +**Merged pull requests:** + +- remove unused dependencies [\#145](https://github.com/getlantern/systray/pull/145) ([joesis](https://github.com/joesis)) + +## [v1.0.1](https://github.com/getlantern/systray/tree/v1.0.1) (2020-05-18) + +[Full Changelog](https://github.com/getlantern/systray/compare/1.0.1...v1.0.1) + +## [1.0.1](https://github.com/getlantern/systray/tree/1.0.1) (2020-05-18) + +[Full Changelog](https://github.com/getlantern/systray/compare/1.0.0...1.0.1) + +**Merged pull requests:** + +- Unlock menuItemsLock before changing UI [\#144](https://github.com/getlantern/systray/pull/144) ([joesis](https://github.com/joesis)) + +## [1.0.0](https://github.com/getlantern/systray/tree/1.0.0) (2020-05-18) + +[Full Changelog](https://github.com/getlantern/systray/compare/v1.0.0...1.0.0) + +## [v1.0.0](https://github.com/getlantern/systray/tree/v1.0.0) (2020-05-18) + +[Full Changelog](https://github.com/getlantern/systray/compare/0.9.0...v1.0.0) + +**Merged pull requests:** + +- Check if the menu item is nil [\#137](https://github.com/getlantern/systray/pull/137) ([myleshorton](https://github.com/myleshorton)) + +## [0.9.0](https://github.com/getlantern/systray/tree/0.9.0) (2020-03-24) + +[Full Changelog](https://github.com/getlantern/systray/compare/v0.9.0...0.9.0) + +## [v0.9.0](https://github.com/getlantern/systray/tree/v0.9.0) (2020-03-24) + +[Full Changelog](https://github.com/getlantern/systray/compare/8e63b37ef27d94f6db79c4ffb941608e8f0dc2f9...v0.9.0) + +**Merged pull requests:** + +- Backport all features and fixes from master [\#140](https://github.com/getlantern/systray/pull/140) ([joesis](https://github.com/joesis)) +- Nested menu windows [\#132](https://github.com/getlantern/systray/pull/132) ([joesis](https://github.com/joesis)) +- Support for nested sub-menus on OS X [\#131](https://github.com/getlantern/systray/pull/131) ([oxtoacart](https://github.com/oxtoacart)) +- Use temp directory for walk resource manager [\#129](https://github.com/getlantern/systray/pull/129) ([max-b](https://github.com/max-b)) +- Added support for template icons on macOS [\#119](https://github.com/getlantern/systray/pull/119) ([oxtoacart](https://github.com/oxtoacart)) +- When launching app window on macOS, make application a foreground app… [\#118](https://github.com/getlantern/systray/pull/118) ([oxtoacart](https://github.com/oxtoacart)) +- Include stdlib.h in systray\_browser\_linux to explicitly declare funct… [\#114](https://github.com/getlantern/systray/pull/114) ([oxtoacart](https://github.com/oxtoacart)) +- Fix panic when resources root path is not the working directory [\#112](https://github.com/getlantern/systray/pull/112) ([ksubileau](https://github.com/ksubileau)) +- Don't print close reason to console [\#111](https://github.com/getlantern/systray/pull/111) ([ksubileau](https://github.com/ksubileau)) +- Systray icon could not be changed dynamically [\#110](https://github.com/getlantern/systray/pull/110) ([ksubileau](https://github.com/ksubileau)) +- Preventing deadlock on menu item ClickeCh when no one is listening, c… [\#105](https://github.com/getlantern/systray/pull/105) ([oxtoacart](https://github.com/oxtoacart)) +- Reverted deadlock fix \(Affected other receivers\) [\#104](https://github.com/getlantern/systray/pull/104) ([ldstein](https://github.com/ldstein)) +- Fix Deadlock and item ordering in Windows [\#103](https://github.com/getlantern/systray/pull/103) ([ldstein](https://github.com/ldstein)) +- Minor README improvements \(go modules, example app, screenshot\) [\#98](https://github.com/getlantern/systray/pull/98) ([tstromberg](https://github.com/tstromberg)) +- Add support for app window [\#97](https://github.com/getlantern/systray/pull/97) ([oxtoacart](https://github.com/oxtoacart)) +- systray\_darwin.m: Compare Mac OS min version with value instead of macro [\#94](https://github.com/getlantern/systray/pull/94) ([teddywing](https://github.com/teddywing)) +- Attempt to fix https://github.com/getlantern/systray/issues/75 [\#92](https://github.com/getlantern/systray/pull/92) ([mikeschinkel](https://github.com/mikeschinkel)) +- Fix application path for MacOS in README [\#91](https://github.com/getlantern/systray/pull/91) ([zereraz](https://github.com/zereraz)) +- Document cross-platform console window details [\#81](https://github.com/getlantern/systray/pull/81) ([michaelsanford](https://github.com/michaelsanford)) +- Fix bad-looking system tray icon in Windows [\#78](https://github.com/getlantern/systray/pull/78) ([juja256](https://github.com/juja256)) +- Add the separator to the visible items [\#76](https://github.com/getlantern/systray/pull/76) ([meskio](https://github.com/meskio)) +- keep track of hidden items [\#74](https://github.com/getlantern/systray/pull/74) ([kalikaneko](https://github.com/kalikaneko)) +- Support macOS older than 10.13 [\#73](https://github.com/getlantern/systray/pull/73) ([swznd](https://github.com/swznd)) +- define ERROR\_SUCCESS as syscall.Errno [\#69](https://github.com/getlantern/systray/pull/69) ([joesis](https://github.com/joesis)) +- Bug/fix broken menuitem show [\#68](https://github.com/getlantern/systray/pull/68) ([kalikaneko](https://github.com/kalikaneko)) +- Fix mac deprecations [\#66](https://github.com/getlantern/systray/pull/66) ([jefvel](https://github.com/jefvel)) +- Made it possible to add icons to menu items on Mac [\#65](https://github.com/getlantern/systray/pull/65) ([jefvel](https://github.com/jefvel)) +- linux: delete temp files as soon as they are not needed [\#63](https://github.com/getlantern/systray/pull/63) ([meskio](https://github.com/meskio)) +- Merge changes from amkulikov to remove DLL for windows [\#56](https://github.com/getlantern/systray/pull/56) ([oxtoacart](https://github.com/oxtoacart)) +- Revert "Use templated icons for the menu bar in macOS" [\#51](https://github.com/getlantern/systray/pull/51) ([stoggi](https://github.com/stoggi)) +- Use templated icons for the menu bar in macOS [\#46](https://github.com/getlantern/systray/pull/46) ([stoggi](https://github.com/stoggi)) +- Syscalls instead of custom DLLs [\#44](https://github.com/getlantern/systray/pull/44) ([amkulikov](https://github.com/amkulikov)) +- On quit exit main loop on linux [\#41](https://github.com/getlantern/systray/pull/41) ([meskio](https://github.com/meskio)) +- Fixed hide show in linux \(\#37\) [\#39](https://github.com/getlantern/systray/pull/39) ([meskio](https://github.com/meskio)) +- fix: linux compilation warning [\#36](https://github.com/getlantern/systray/pull/36) ([novln](https://github.com/novln)) +- Added separator functionality [\#32](https://github.com/getlantern/systray/pull/32) ([oxtoacart](https://github.com/oxtoacart)) + + + +\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* diff --git a/vendor/fyne.io/systray/LICENSE b/vendor/fyne.io/systray/LICENSE new file mode 100644 index 0000000..3ee0162 --- /dev/null +++ b/vendor/fyne.io/systray/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2014 Brave New Software Project, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/fyne.io/systray/Makefile b/vendor/fyne.io/systray/Makefile new file mode 100644 index 0000000..12f3d22 --- /dev/null +++ b/vendor/fyne.io/systray/Makefile @@ -0,0 +1,18 @@ +tag-changelog: require-version require-gh-token + echo "Tagging..." && \ + git tag -a "$$VERSION" -f --annotate -m"Tagged $$VERSION" && \ + git push --tags -f && \ + git checkout master && \ + git pull && \ + github_changelog_generator --no-issues --max-issues 100 --token "${GH_TOKEN}" --user getlantern --project systray && \ + git add CHANGELOG.md && \ + git commit -m "Updated changelog for $$VERSION" && \ + git push origin HEAD && \ + git checkout - + +guard-%: + @ if [ -z '${${*}}' ]; then echo 'Environment variable $* not set' && exit 1; fi + +require-version: guard-VERSION + +require-gh-token: guard-GH_TOKEN diff --git a/vendor/fyne.io/systray/README.md b/vendor/fyne.io/systray/README.md new file mode 100644 index 0000000..6881e95 --- /dev/null +++ b/vendor/fyne.io/systray/README.md @@ -0,0 +1,147 @@ +# Systray + +systray is a cross-platform Go library to place an icon and menu in the notification area. +This repository is a fork of [getlantern/systray](https://github.com/getlantern/systray) +removing the GTK dependency and support for legacy linux system tray. + +## Features + +* Supported on Windows, macOS, Linux and many BSD systems +* Menu items can be checked and/or disabled +* Methods may be called from any Goroutine + +## API + +```go +package main + +import "fyne.io/systray" +import "fyne.io/systray/example/icon" + +func main() { + systray.Run(onReady, onExit) +} + +func onReady() { + systray.SetIcon(icon.Data) + systray.SetTitle("Awesome App") + systray.SetTooltip("Pretty awesome超级棒") + mQuit := systray.AddMenuItem("Quit", "Quit the whole app") + + // Sets the icon of a menu item. + mQuit.SetIcon(icon.Data) +} + +func onExit() { + // clean up here +} +``` + +### Running in a Fyne app + +This repository is designed to allow any toolkit to integrate system tray without any additional dependencies. +It is maintained by the Fyne team, but if you are using Fyne there is an even easier to use API in the main repository that wraps this project. + +In your app you can use a standard `fyne.Menu` structure and pass it to `SetSystemTrayMenu` when your app is a desktop app, as follows: + +```go + menu := fyne.NewMenu("MyApp", + fyne.NewMenuItem("Show", func() { + log.Println("Tapped show") + })) + + if desk, ok := myApp.(desktop.App); ok { + desk.SetSystemTrayMenu(menu) + } +``` + +You can find out more in the toolkit documentation: +[System Tray Menu](https://developer.fyne.io/explore/systray). + +### Run in another toolkit + +Most graphical toolkits will grab the main loop so the `Run` code above is not possible. +For this reason there is another entry point `RunWithExternalLoop`. +This function of the library returns a start and end function that should be called +when the application has started and will end, to loop in appropriate features. + +See [full API](https://pkg.go.dev/fyne.io/systray?tab=doc) as well as [CHANGELOG](https://github.com/fyne-io/systray/tree/master/CHANGELOG.md). + +Note: this package requires cgo, so make sure you set `CGO_ENABLED=1` before building. + +## Try the example app! + +Have go v1.12+ or higher installed? Here's an example to get started on macOS or Linux: + +```sh +git clone https://github.com/fyne-io/systray +cd systray/example +go run . +``` + +On Windows, you should follow the instructions above, but use the followign run command: + +``` +go run -ldflags "-H=windowsgui" . +``` + +Now look for *Awesome App* in your menu bar! + +![Awesome App screenshot](example/screenshot.png) + +## Platform notes + +### Linux/BSD + +This implementation uses DBus to communicate through the SystemNotifier/AppIndicator spec, older tray implementations may not load the icon. + +If you are running an older desktop environment, or system tray provider, you may require a proxy app which can convert the new DBus calls to the old format. +The recommended tool for Gnome based trays is [snixembed](https://git.sr.ht/~steef/snixembed), others are available. +Search for "StatusNotifierItems XEmbedded" in your package manager. + +### Windows + +* To avoid opening a console at application startup, use "fyne package" for your app or manually use these compile flags: + +```sh +go build -ldflags -H=windowsgui +``` + +### macOS + +On macOS, you will need to create an application bundle to wrap the binary; simply use "fyne package" or add folders with the following minimal structure and assets: + +``` +SystrayApp.app/ + Contents/ + Info.plist + MacOS/ + go-executable + Resources/ + SystrayApp.icns +``` + +If bundling manually, you may want to add one or both of the following to your Info.plist: + +```xml + + NSHighResolutionCapable + True + + + LSUIElement + 1 +``` + +Consult the [Official Apple Documentation here](https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html#//apple_ref/doc/uid/10000123i-CH101-SW1). + +On macOS, it's possible to set the underlying +[`NSStatusItemBehavior`](https://developer.apple.com/documentation/appkit/nsstatusitembehavior?language=objc) +with `systray.SetRemovalAllowed(true)`. When enabled, the user can cmd-drag the +icon off the menu bar. + +## Credits + +- https://github.com/getlantern/systray +- https://github.com/xilp/systray +- https://github.com/cratonica/trayhost diff --git a/vendor/fyne.io/systray/internal/generated/menu/dbus_menu.go b/vendor/fyne.io/systray/internal/generated/menu/dbus_menu.go new file mode 100644 index 0000000..1b896d3 --- /dev/null +++ b/vendor/fyne.io/systray/internal/generated/menu/dbus_menu.go @@ -0,0 +1,484 @@ +// Code generated by dbus-codegen-go DO NOT EDIT. +package menu + +import ( + "context" + "errors" + "fmt" + + "github.com/godbus/dbus/v5" + "github.com/godbus/dbus/v5/introspect" +) + +var ( + // Introspection for com.canonical.dbusmenu + IntrospectDataDbusmenu = introspect.Interface{ + Name: "com.canonical.dbusmenu", + Methods: []introspect.Method{{Name: "GetLayout", Args: []introspect.Arg{ + {Name: "parentId", Type: "i", Direction: "in"}, + {Name: "recursionDepth", Type: "i", Direction: "in"}, + {Name: "propertyNames", Type: "as", Direction: "in"}, + {Name: "revision", Type: "u", Direction: "out"}, + {Name: "layout", Type: "(ia{sv}av)", Direction: "out"}, + }}, + {Name: "GetGroupProperties", Args: []introspect.Arg{ + {Name: "ids", Type: "ai", Direction: "in"}, + {Name: "propertyNames", Type: "as", Direction: "in"}, + {Name: "properties", Type: "a(ia{sv})", Direction: "out"}, + }}, + {Name: "GetProperty", Args: []introspect.Arg{ + {Name: "id", Type: "i", Direction: "in"}, + {Name: "name", Type: "s", Direction: "in"}, + {Name: "value", Type: "v", Direction: "out"}, + }}, + {Name: "Event", Args: []introspect.Arg{ + {Name: "id", Type: "i", Direction: "in"}, + {Name: "eventId", Type: "s", Direction: "in"}, + {Name: "data", Type: "v", Direction: "in"}, + {Name: "timestamp", Type: "u", Direction: "in"}, + }}, + {Name: "EventGroup", Args: []introspect.Arg{ + {Name: "events", Type: "a(isvu)", Direction: "in"}, + {Name: "idErrors", Type: "ai", Direction: "out"}, + }}, + {Name: "AboutToShow", Args: []introspect.Arg{ + {Name: "id", Type: "i", Direction: "in"}, + {Name: "needUpdate", Type: "b", Direction: "out"}, + }}, + {Name: "AboutToShowGroup", Args: []introspect.Arg{ + {Name: "ids", Type: "ai", Direction: "in"}, + {Name: "updatesNeeded", Type: "ai", Direction: "out"}, + {Name: "idErrors", Type: "ai", Direction: "out"}, + }}, + }, + Signals: []introspect.Signal{{Name: "ItemsPropertiesUpdated", Args: []introspect.Arg{ + {Name: "updatedProps", Type: "a(ia{sv})", Direction: "out"}, + {Name: "removedProps", Type: "a(ias)", Direction: "out"}, + }}, + {Name: "LayoutUpdated", Args: []introspect.Arg{ + {Name: "revision", Type: "u", Direction: "out"}, + {Name: "parent", Type: "i", Direction: "out"}, + }}, + {Name: "ItemActivationRequested", Args: []introspect.Arg{ + {Name: "id", Type: "i", Direction: "out"}, + {Name: "timestamp", Type: "u", Direction: "out"}, + }}, + }, + Properties: []introspect.Property{{Name: "Version", Type: "u", Access: "read"}, + {Name: "TextDirection", Type: "s", Access: "read"}, + {Name: "Status", Type: "s", Access: "read"}, + {Name: "IconThemePath", Type: "as", Access: "read"}, + }, + Annotations: []introspect.Annotation{}, + } +) + +// Signal is a common interface for all signals. +type Signal interface { + Name() string + Interface() string + Sender() string + + path() dbus.ObjectPath + values() []interface{} +} + +// Emit sends the given signal to the bus. +func Emit(conn *dbus.Conn, s Signal) error { + return conn.Emit(s.path(), s.Interface()+"."+s.Name(), s.values()...) +} + +// ErrUnknownSignal is returned by LookupSignal when a signal cannot be resolved. +var ErrUnknownSignal = errors.New("unknown signal") + +// LookupSignal converts the given raw D-Bus signal with variable body +// into one with typed structured body or returns ErrUnknownSignal error. +func LookupSignal(signal *dbus.Signal) (Signal, error) { + switch signal.Name { + case InterfaceDbusmenu + "." + "ItemsPropertiesUpdated": + v0, ok := signal.Body[0].([]struct { + V0 int32 + V1 map[string]dbus.Variant + }) + if !ok { + return nil, fmt.Errorf("prop .UpdatedProps is %T, not []struct {V0 int32;V1 map[string]dbus.Variant}", signal.Body[0]) + } + v1, ok := signal.Body[1].([]struct { + V0 int32 + V1 []string + }) + if !ok { + return nil, fmt.Errorf("prop .RemovedProps is %T, not []struct {V0 int32;V1 []string}", signal.Body[1]) + } + return &Dbusmenu_ItemsPropertiesUpdatedSignal{ + sender: signal.Sender, + Path: signal.Path, + Body: &Dbusmenu_ItemsPropertiesUpdatedSignalBody{ + UpdatedProps: v0, + RemovedProps: v1, + }, + }, nil + case InterfaceDbusmenu + "." + "LayoutUpdated": + v0, ok := signal.Body[0].(uint32) + if !ok { + return nil, fmt.Errorf("prop .Revision is %T, not uint32", signal.Body[0]) + } + v1, ok := signal.Body[1].(int32) + if !ok { + return nil, fmt.Errorf("prop .Parent is %T, not int32", signal.Body[1]) + } + return &Dbusmenu_LayoutUpdatedSignal{ + sender: signal.Sender, + Path: signal.Path, + Body: &Dbusmenu_LayoutUpdatedSignalBody{ + Revision: v0, + Parent: v1, + }, + }, nil + case InterfaceDbusmenu + "." + "ItemActivationRequested": + v0, ok := signal.Body[0].(int32) + if !ok { + return nil, fmt.Errorf("prop .Id is %T, not int32", signal.Body[0]) + } + v1, ok := signal.Body[1].(uint32) + if !ok { + return nil, fmt.Errorf("prop .Timestamp is %T, not uint32", signal.Body[1]) + } + return &Dbusmenu_ItemActivationRequestedSignal{ + sender: signal.Sender, + Path: signal.Path, + Body: &Dbusmenu_ItemActivationRequestedSignalBody{ + Id: v0, + Timestamp: v1, + }, + }, nil + default: + return nil, ErrUnknownSignal + } +} + +// AddMatchSignal registers a match rule for the given signal, +// opts are appended to the automatically generated signal's rules. +func AddMatchSignal(conn *dbus.Conn, s Signal, opts ...dbus.MatchOption) error { + return conn.AddMatchSignal(append([]dbus.MatchOption{ + dbus.WithMatchInterface(s.Interface()), + dbus.WithMatchMember(s.Name()), + }, opts...)...) +} + +// RemoveMatchSignal unregisters the previously registered subscription. +func RemoveMatchSignal(conn *dbus.Conn, s Signal, opts ...dbus.MatchOption) error { + return conn.RemoveMatchSignal(append([]dbus.MatchOption{ + dbus.WithMatchInterface(s.Interface()), + dbus.WithMatchMember(s.Name()), + }, opts...)...) +} + +// Interface name constants. +const ( + InterfaceDbusmenu = "com.canonical.dbusmenu" +) + +// Dbusmenuer is com.canonical.dbusmenu interface. +type Dbusmenuer interface { + // GetLayout is com.canonical.dbusmenu.GetLayout method. + GetLayout(parentId int32, recursionDepth int32, propertyNames []string) (revision uint32, layout struct { + V0 int32 + V1 map[string]dbus.Variant + V2 []dbus.Variant + }, err *dbus.Error) + // GetGroupProperties is com.canonical.dbusmenu.GetGroupProperties method. + GetGroupProperties(ids []int32, propertyNames []string) (properties []struct { + V0 int32 + V1 map[string]dbus.Variant + }, err *dbus.Error) + // GetProperty is com.canonical.dbusmenu.GetProperty method. + GetProperty(id int32, name string) (value dbus.Variant, err *dbus.Error) + // Event is com.canonical.dbusmenu.Event method. + Event(id int32, eventId string, data dbus.Variant, timestamp uint32) (err *dbus.Error) + // EventGroup is com.canonical.dbusmenu.EventGroup method. + EventGroup(events []struct { + V0 int32 + V1 string + V2 dbus.Variant + V3 uint32 + }) (idErrors []int32, err *dbus.Error) + // AboutToShow is com.canonical.dbusmenu.AboutToShow method. + AboutToShow(id int32) (needUpdate bool, err *dbus.Error) + // AboutToShowGroup is com.canonical.dbusmenu.AboutToShowGroup method. + AboutToShowGroup(ids []int32) (updatesNeeded []int32, idErrors []int32, err *dbus.Error) +} + +// ExportDbusmenu exports the given object that implements com.canonical.dbusmenu on the bus. +func ExportDbusmenu(conn *dbus.Conn, path dbus.ObjectPath, v Dbusmenuer) error { + return conn.ExportSubtreeMethodTable(map[string]interface{}{ + "GetLayout": v.GetLayout, + "GetGroupProperties": v.GetGroupProperties, + "GetProperty": v.GetProperty, + "Event": v.Event, + "EventGroup": v.EventGroup, + "AboutToShow": v.AboutToShow, + "AboutToShowGroup": v.AboutToShowGroup, + }, path, InterfaceDbusmenu) +} + +// UnexportDbusmenu unexports com.canonical.dbusmenu interface on the named path. +func UnexportDbusmenu(conn *dbus.Conn, path dbus.ObjectPath) error { + return conn.Export(nil, path, InterfaceDbusmenu) +} + +// UnimplementedDbusmenu can be embedded to have forward compatible server implementations. +type UnimplementedDbusmenu struct{} + +func (*UnimplementedDbusmenu) iface() string { + return InterfaceDbusmenu +} + +func (*UnimplementedDbusmenu) GetLayout(parentId int32, recursionDepth int32, propertyNames []string) (revision uint32, layout struct { + V0 int32 + V1 map[string]dbus.Variant + V2 []dbus.Variant +}, err *dbus.Error) { + err = &dbus.ErrMsgUnknownMethod + return +} + +func (*UnimplementedDbusmenu) GetGroupProperties(ids []int32, propertyNames []string) (properties []struct { + V0 int32 + V1 map[string]dbus.Variant +}, err *dbus.Error) { + err = &dbus.ErrMsgUnknownMethod + return +} + +func (*UnimplementedDbusmenu) GetProperty(id int32, name string) (value dbus.Variant, err *dbus.Error) { + err = &dbus.ErrMsgUnknownMethod + return +} + +func (*UnimplementedDbusmenu) Event(id int32, eventId string, data dbus.Variant, timestamp uint32) (err *dbus.Error) { + err = &dbus.ErrMsgUnknownMethod + return +} + +func (*UnimplementedDbusmenu) EventGroup(events []struct { + V0 int32 + V1 string + V2 dbus.Variant + V3 uint32 +}) (idErrors []int32, err *dbus.Error) { + err = &dbus.ErrMsgUnknownMethod + return +} + +func (*UnimplementedDbusmenu) AboutToShow(id int32) (needUpdate bool, err *dbus.Error) { + err = &dbus.ErrMsgUnknownMethod + return +} + +func (*UnimplementedDbusmenu) AboutToShowGroup(ids []int32) (updatesNeeded []int32, idErrors []int32, err *dbus.Error) { + err = &dbus.ErrMsgUnknownMethod + return +} + +// NewDbusmenu creates and allocates com.canonical.dbusmenu. +func NewDbusmenu(object dbus.BusObject) *Dbusmenu { + return &Dbusmenu{object} +} + +// Dbusmenu implements com.canonical.dbusmenu D-Bus interface. +type Dbusmenu struct { + object dbus.BusObject +} + +// GetLayout calls com.canonical.dbusmenu.GetLayout method. +func (o *Dbusmenu) GetLayout(ctx context.Context, parentId int32, recursionDepth int32, propertyNames []string) (revision uint32, layout struct { + V0 int32 + V1 map[string]dbus.Variant + V2 []dbus.Variant +}, err error) { + err = o.object.CallWithContext(ctx, InterfaceDbusmenu+".GetLayout", 0, parentId, recursionDepth, propertyNames).Store(&revision, &layout) + return +} + +// GetGroupProperties calls com.canonical.dbusmenu.GetGroupProperties method. +func (o *Dbusmenu) GetGroupProperties(ctx context.Context, ids []int32, propertyNames []string) (properties []struct { + V0 int32 + V1 map[string]dbus.Variant +}, err error) { + err = o.object.CallWithContext(ctx, InterfaceDbusmenu+".GetGroupProperties", 0, ids, propertyNames).Store(&properties) + return +} + +// GetProperty calls com.canonical.dbusmenu.GetProperty method. +func (o *Dbusmenu) GetProperty(ctx context.Context, id int32, name string) (value dbus.Variant, err error) { + err = o.object.CallWithContext(ctx, InterfaceDbusmenu+".GetProperty", 0, id, name).Store(&value) + return +} + +// Event calls com.canonical.dbusmenu.Event method. +func (o *Dbusmenu) Event(ctx context.Context, id int32, eventId string, data dbus.Variant, timestamp uint32) (err error) { + err = o.object.CallWithContext(ctx, InterfaceDbusmenu+".Event", 0, id, eventId, data, timestamp).Store() + return +} + +// EventGroup calls com.canonical.dbusmenu.EventGroup method. +func (o *Dbusmenu) EventGroup(ctx context.Context, events []struct { + V0 int32 + V1 string + V2 dbus.Variant + V3 uint32 +}) (idErrors []int32, err error) { + err = o.object.CallWithContext(ctx, InterfaceDbusmenu+".EventGroup", 0, events).Store(&idErrors) + return +} + +// AboutToShow calls com.canonical.dbusmenu.AboutToShow method. +func (o *Dbusmenu) AboutToShow(ctx context.Context, id int32) (needUpdate bool, err error) { + err = o.object.CallWithContext(ctx, InterfaceDbusmenu+".AboutToShow", 0, id).Store(&needUpdate) + return +} + +// AboutToShowGroup calls com.canonical.dbusmenu.AboutToShowGroup method. +func (o *Dbusmenu) AboutToShowGroup(ctx context.Context, ids []int32) (updatesNeeded []int32, idErrors []int32, err error) { + err = o.object.CallWithContext(ctx, InterfaceDbusmenu+".AboutToShowGroup", 0, ids).Store(&updatesNeeded, &idErrors) + return +} + +// GetVersion gets com.canonical.dbusmenu.Version property. +func (o *Dbusmenu) GetVersion(ctx context.Context) (version uint32, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceDbusmenu, "Version").Store(&version) + return +} + +// GetTextDirection gets com.canonical.dbusmenu.TextDirection property. +func (o *Dbusmenu) GetTextDirection(ctx context.Context) (textDirection string, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceDbusmenu, "TextDirection").Store(&textDirection) + return +} + +// GetStatus gets com.canonical.dbusmenu.Status property. +func (o *Dbusmenu) GetStatus(ctx context.Context) (status string, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceDbusmenu, "Status").Store(&status) + return +} + +// GetIconThemePath gets com.canonical.dbusmenu.IconThemePath property. +func (o *Dbusmenu) GetIconThemePath(ctx context.Context) (iconThemePath []string, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceDbusmenu, "IconThemePath").Store(&iconThemePath) + return +} + +// Dbusmenu_ItemsPropertiesUpdatedSignal represents com.canonical.dbusmenu.ItemsPropertiesUpdated signal. +type Dbusmenu_ItemsPropertiesUpdatedSignal struct { + sender string + Path dbus.ObjectPath + Body *Dbusmenu_ItemsPropertiesUpdatedSignalBody +} + +// Name returns the signal's name. +func (s *Dbusmenu_ItemsPropertiesUpdatedSignal) Name() string { + return "ItemsPropertiesUpdated" +} + +// Interface returns the signal's interface. +func (s *Dbusmenu_ItemsPropertiesUpdatedSignal) Interface() string { + return InterfaceDbusmenu +} + +// Sender returns the signal's sender unique name. +func (s *Dbusmenu_ItemsPropertiesUpdatedSignal) Sender() string { + return s.sender +} + +func (s *Dbusmenu_ItemsPropertiesUpdatedSignal) path() dbus.ObjectPath { + return s.Path +} + +func (s *Dbusmenu_ItemsPropertiesUpdatedSignal) values() []interface{} { + return []interface{}{s.Body.UpdatedProps, s.Body.RemovedProps} +} + +// Dbusmenu_ItemsPropertiesUpdatedSignalBody is body container. +type Dbusmenu_ItemsPropertiesUpdatedSignalBody struct { + UpdatedProps []struct { + V0 int32 + V1 map[string]dbus.Variant + } + RemovedProps []struct { + V0 int32 + V1 []string + } +} + +// Dbusmenu_LayoutUpdatedSignal represents com.canonical.dbusmenu.LayoutUpdated signal. +type Dbusmenu_LayoutUpdatedSignal struct { + sender string + Path dbus.ObjectPath + Body *Dbusmenu_LayoutUpdatedSignalBody +} + +// Name returns the signal's name. +func (s *Dbusmenu_LayoutUpdatedSignal) Name() string { + return "LayoutUpdated" +} + +// Interface returns the signal's interface. +func (s *Dbusmenu_LayoutUpdatedSignal) Interface() string { + return InterfaceDbusmenu +} + +// Sender returns the signal's sender unique name. +func (s *Dbusmenu_LayoutUpdatedSignal) Sender() string { + return s.sender +} + +func (s *Dbusmenu_LayoutUpdatedSignal) path() dbus.ObjectPath { + return s.Path +} + +func (s *Dbusmenu_LayoutUpdatedSignal) values() []interface{} { + return []interface{}{s.Body.Revision, s.Body.Parent} +} + +// Dbusmenu_LayoutUpdatedSignalBody is body container. +type Dbusmenu_LayoutUpdatedSignalBody struct { + Revision uint32 + Parent int32 +} + +// Dbusmenu_ItemActivationRequestedSignal represents com.canonical.dbusmenu.ItemActivationRequested signal. +type Dbusmenu_ItemActivationRequestedSignal struct { + sender string + Path dbus.ObjectPath + Body *Dbusmenu_ItemActivationRequestedSignalBody +} + +// Name returns the signal's name. +func (s *Dbusmenu_ItemActivationRequestedSignal) Name() string { + return "ItemActivationRequested" +} + +// Interface returns the signal's interface. +func (s *Dbusmenu_ItemActivationRequestedSignal) Interface() string { + return InterfaceDbusmenu +} + +// Sender returns the signal's sender unique name. +func (s *Dbusmenu_ItemActivationRequestedSignal) Sender() string { + return s.sender +} + +func (s *Dbusmenu_ItemActivationRequestedSignal) path() dbus.ObjectPath { + return s.Path +} + +func (s *Dbusmenu_ItemActivationRequestedSignal) values() []interface{} { + return []interface{}{s.Body.Id, s.Body.Timestamp} +} + +// Dbusmenu_ItemActivationRequestedSignalBody is body container. +type Dbusmenu_ItemActivationRequestedSignalBody struct { + Id int32 + Timestamp uint32 +} diff --git a/vendor/fyne.io/systray/internal/generated/notifier/status_notifier_item.go b/vendor/fyne.io/systray/internal/generated/notifier/status_notifier_item.go new file mode 100644 index 0000000..230c40d --- /dev/null +++ b/vendor/fyne.io/systray/internal/generated/notifier/status_notifier_item.go @@ -0,0 +1,633 @@ +// Code generated by dbus-codegen-go DO NOT EDIT. +package notifier + +import ( + "context" + "errors" + "fmt" + + "github.com/godbus/dbus/v5" + "github.com/godbus/dbus/v5/introspect" +) + +var ( + // Introspection for org.kde.StatusNotifierItem + IntrospectDataStatusNotifierItem = introspect.Interface{ + Name: "org.kde.StatusNotifierItem", + Methods: []introspect.Method{{Name: "ContextMenu", Args: []introspect.Arg{ + {Name: "x", Type: "i", Direction: "in"}, + {Name: "y", Type: "i", Direction: "in"}, + }}, + {Name: "Activate", Args: []introspect.Arg{ + {Name: "x", Type: "i", Direction: "in"}, + {Name: "y", Type: "i", Direction: "in"}, + }}, + {Name: "SecondaryActivate", Args: []introspect.Arg{ + {Name: "x", Type: "i", Direction: "in"}, + {Name: "y", Type: "i", Direction: "in"}, + }}, + {Name: "Scroll", Args: []introspect.Arg{ + {Name: "delta", Type: "i", Direction: "in"}, + {Name: "orientation", Type: "s", Direction: "in"}, + }}, + }, + Signals: []introspect.Signal{{Name: "NewTitle"}, + {Name: "NewIcon"}, + {Name: "NewAttentionIcon"}, + {Name: "NewOverlayIcon"}, + {Name: "NewStatus", Args: []introspect.Arg{ + {Name: "status", Type: "s", Direction: ""}, + }}, + {Name: "NewIconThemePath", Args: []introspect.Arg{ + {Name: "icon_theme_path", Type: "s", Direction: "out"}, + }}, + {Name: "NewMenu"}, + }, + Properties: []introspect.Property{{Name: "Category", Type: "s", Access: "read"}, + {Name: "Id", Type: "s", Access: "read"}, + {Name: "Title", Type: "s", Access: "read"}, + {Name: "Status", Type: "s", Access: "read"}, + {Name: "WindowId", Type: "i", Access: "read"}, + {Name: "IconThemePath", Type: "s", Access: "read"}, + {Name: "Menu", Type: "o", Access: "read"}, + {Name: "ItemIsMenu", Type: "b", Access: "read"}, + {Name: "IconName", Type: "s", Access: "read"}, + {Name: "IconPixmap", Type: "a(iiay)", Access: "read", Annotations: []introspect.Annotation{ + {Name: "org.qtproject.QtDBus.QtTypeName", Value: "KDbusImageVector"}, + }}, + {Name: "OverlayIconName", Type: "s", Access: "read"}, + {Name: "OverlayIconPixmap", Type: "a(iiay)", Access: "read", Annotations: []introspect.Annotation{ + {Name: "org.qtproject.QtDBus.QtTypeName", Value: "KDbusImageVector"}, + }}, + {Name: "AttentionIconName", Type: "s", Access: "read"}, + {Name: "AttentionIconPixmap", Type: "a(iiay)", Access: "read", Annotations: []introspect.Annotation{ + {Name: "org.qtproject.QtDBus.QtTypeName", Value: "KDbusImageVector"}, + }}, + {Name: "AttentionMovieName", Type: "s", Access: "read"}, + {Name: "ToolTip", Type: "(sa(iiay)ss)", Access: "read", Annotations: []introspect.Annotation{ + {Name: "org.qtproject.QtDBus.QtTypeName", Value: "KDbusToolTipStruct"}, + }}, + }, + Annotations: []introspect.Annotation{}, + } +) + +// Signal is a common interface for all signals. +type Signal interface { + Name() string + Interface() string + Sender() string + + path() dbus.ObjectPath + values() []interface{} +} + +// Emit sends the given signal to the bus. +func Emit(conn *dbus.Conn, s Signal) error { + return conn.Emit(s.path(), s.Interface()+"."+s.Name(), s.values()...) +} + +// ErrUnknownSignal is returned by LookupSignal when a signal cannot be resolved. +var ErrUnknownSignal = errors.New("unknown signal") + +// LookupSignal converts the given raw D-Bus signal with variable body +// into one with typed structured body or returns ErrUnknownSignal error. +func LookupSignal(signal *dbus.Signal) (Signal, error) { + switch signal.Name { + case InterfaceStatusNotifierItem + "." + "NewTitle": + return &StatusNotifierItem_NewTitleSignal{ + sender: signal.Sender, + Path: signal.Path, + Body: &StatusNotifierItem_NewTitleSignalBody{}, + }, nil + case InterfaceStatusNotifierItem + "." + "NewIcon": + return &StatusNotifierItem_NewIconSignal{ + sender: signal.Sender, + Path: signal.Path, + Body: &StatusNotifierItem_NewIconSignalBody{}, + }, nil + case InterfaceStatusNotifierItem + "." + "NewAttentionIcon": + return &StatusNotifierItem_NewAttentionIconSignal{ + sender: signal.Sender, + Path: signal.Path, + Body: &StatusNotifierItem_NewAttentionIconSignalBody{}, + }, nil + case InterfaceStatusNotifierItem + "." + "NewOverlayIcon": + return &StatusNotifierItem_NewOverlayIconSignal{ + sender: signal.Sender, + Path: signal.Path, + Body: &StatusNotifierItem_NewOverlayIconSignalBody{}, + }, nil + case InterfaceStatusNotifierItem + "." + "NewStatus": + v0, ok := signal.Body[0].(string) + if !ok { + return nil, fmt.Errorf("prop .Status is %T, not string", signal.Body[0]) + } + return &StatusNotifierItem_NewStatusSignal{ + sender: signal.Sender, + Path: signal.Path, + Body: &StatusNotifierItem_NewStatusSignalBody{ + Status: v0, + }, + }, nil + case InterfaceStatusNotifierItem + "." + "NewIconThemePath": + v0, ok := signal.Body[0].(string) + if !ok { + return nil, fmt.Errorf("prop .IconThemePath is %T, not string", signal.Body[0]) + } + return &StatusNotifierItem_NewIconThemePathSignal{ + sender: signal.Sender, + Path: signal.Path, + Body: &StatusNotifierItem_NewIconThemePathSignalBody{ + IconThemePath: v0, + }, + }, nil + case InterfaceStatusNotifierItem + "." + "NewMenu": + return &StatusNotifierItem_NewMenuSignal{ + sender: signal.Sender, + Path: signal.Path, + Body: &StatusNotifierItem_NewMenuSignalBody{}, + }, nil + default: + return nil, ErrUnknownSignal + } +} + +// AddMatchSignal registers a match rule for the given signal, +// opts are appended to the automatically generated signal's rules. +func AddMatchSignal(conn *dbus.Conn, s Signal, opts ...dbus.MatchOption) error { + return conn.AddMatchSignal(append([]dbus.MatchOption{ + dbus.WithMatchInterface(s.Interface()), + dbus.WithMatchMember(s.Name()), + }, opts...)...) +} + +// RemoveMatchSignal unregisters the previously registered subscription. +func RemoveMatchSignal(conn *dbus.Conn, s Signal, opts ...dbus.MatchOption) error { + return conn.RemoveMatchSignal(append([]dbus.MatchOption{ + dbus.WithMatchInterface(s.Interface()), + dbus.WithMatchMember(s.Name()), + }, opts...)...) +} + +// Interface name constants. +const ( + InterfaceStatusNotifierItem = "org.kde.StatusNotifierItem" +) + +// StatusNotifierItemer is org.kde.StatusNotifierItem interface. +type StatusNotifierItemer interface { + // ContextMenu is org.kde.StatusNotifierItem.ContextMenu method. + ContextMenu(x int32, y int32) (err *dbus.Error) + // Activate is org.kde.StatusNotifierItem.Activate method. + Activate(x int32, y int32) (err *dbus.Error) + // SecondaryActivate is org.kde.StatusNotifierItem.SecondaryActivate method. + SecondaryActivate(x int32, y int32) (err *dbus.Error) + // Scroll is org.kde.StatusNotifierItem.Scroll method. + Scroll(delta int32, orientation string) (err *dbus.Error) +} + +// ExportStatusNotifierItem exports the given object that implements org.kde.StatusNotifierItem on the bus. +func ExportStatusNotifierItem(conn *dbus.Conn, path dbus.ObjectPath, v StatusNotifierItemer) error { + return conn.ExportSubtreeMethodTable(map[string]interface{}{ + "ContextMenu": v.ContextMenu, + "Activate": v.Activate, + "SecondaryActivate": v.SecondaryActivate, + "Scroll": v.Scroll, + }, path, InterfaceStatusNotifierItem) +} + +// UnexportStatusNotifierItem unexports org.kde.StatusNotifierItem interface on the named path. +func UnexportStatusNotifierItem(conn *dbus.Conn, path dbus.ObjectPath) error { + return conn.Export(nil, path, InterfaceStatusNotifierItem) +} + +// UnimplementedStatusNotifierItem can be embedded to have forward compatible server implementations. +type UnimplementedStatusNotifierItem struct{} + +func (*UnimplementedStatusNotifierItem) iface() string { + return InterfaceStatusNotifierItem +} + +func (*UnimplementedStatusNotifierItem) ContextMenu(x int32, y int32) (err *dbus.Error) { + err = &dbus.ErrMsgUnknownMethod + return +} + +func (*UnimplementedStatusNotifierItem) Activate(x int32, y int32) (err *dbus.Error) { + err = &dbus.ErrMsgUnknownMethod + return +} + +func (*UnimplementedStatusNotifierItem) SecondaryActivate(x int32, y int32) (err *dbus.Error) { + err = &dbus.ErrMsgUnknownMethod + return +} + +func (*UnimplementedStatusNotifierItem) Scroll(delta int32, orientation string) (err *dbus.Error) { + err = &dbus.ErrMsgUnknownMethod + return +} + +// NewStatusNotifierItem creates and allocates org.kde.StatusNotifierItem. +func NewStatusNotifierItem(object dbus.BusObject) *StatusNotifierItem { + return &StatusNotifierItem{object} +} + +// StatusNotifierItem implements org.kde.StatusNotifierItem D-Bus interface. +type StatusNotifierItem struct { + object dbus.BusObject +} + +// ContextMenu calls org.kde.StatusNotifierItem.ContextMenu method. +func (o *StatusNotifierItem) ContextMenu(ctx context.Context, x int32, y int32) (err error) { + err = o.object.CallWithContext(ctx, InterfaceStatusNotifierItem+".ContextMenu", 0, x, y).Store() + return +} + +// Activate calls org.kde.StatusNotifierItem.Activate method. +func (o *StatusNotifierItem) Activate(ctx context.Context, x int32, y int32) (err error) { + err = o.object.CallWithContext(ctx, InterfaceStatusNotifierItem+".Activate", 0, x, y).Store() + return +} + +// SecondaryActivate calls org.kde.StatusNotifierItem.SecondaryActivate method. +func (o *StatusNotifierItem) SecondaryActivate(ctx context.Context, x int32, y int32) (err error) { + err = o.object.CallWithContext(ctx, InterfaceStatusNotifierItem+".SecondaryActivate", 0, x, y).Store() + return +} + +// Scroll calls org.kde.StatusNotifierItem.Scroll method. +func (o *StatusNotifierItem) Scroll(ctx context.Context, delta int32, orientation string) (err error) { + err = o.object.CallWithContext(ctx, InterfaceStatusNotifierItem+".Scroll", 0, delta, orientation).Store() + return +} + +// GetCategory gets org.kde.StatusNotifierItem.Category property. +func (o *StatusNotifierItem) GetCategory(ctx context.Context) (category string, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "Category").Store(&category) + return +} + +// GetId gets org.kde.StatusNotifierItem.Id property. +func (o *StatusNotifierItem) GetId(ctx context.Context) (id string, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "Id").Store(&id) + return +} + +// GetTitle gets org.kde.StatusNotifierItem.Title property. +func (o *StatusNotifierItem) GetTitle(ctx context.Context) (title string, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "Title").Store(&title) + return +} + +// GetStatus gets org.kde.StatusNotifierItem.Status property. +func (o *StatusNotifierItem) GetStatus(ctx context.Context) (status string, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "Status").Store(&status) + return +} + +// GetWindowId gets org.kde.StatusNotifierItem.WindowId property. +func (o *StatusNotifierItem) GetWindowId(ctx context.Context) (windowId int32, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "WindowId").Store(&windowId) + return +} + +// GetIconThemePath gets org.kde.StatusNotifierItem.IconThemePath property. +func (o *StatusNotifierItem) GetIconThemePath(ctx context.Context) (iconThemePath string, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "IconThemePath").Store(&iconThemePath) + return +} + +// GetMenu gets org.kde.StatusNotifierItem.Menu property. +func (o *StatusNotifierItem) GetMenu(ctx context.Context) (menu dbus.ObjectPath, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "Menu").Store(&menu) + return +} + +// GetItemIsMenu gets org.kde.StatusNotifierItem.ItemIsMenu property. +func (o *StatusNotifierItem) GetItemIsMenu(ctx context.Context) (itemIsMenu bool, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "ItemIsMenu").Store(&itemIsMenu) + return +} + +// GetIconName gets org.kde.StatusNotifierItem.IconName property. +func (o *StatusNotifierItem) GetIconName(ctx context.Context) (iconName string, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "IconName").Store(&iconName) + return +} + +// GetIconPixmap gets org.kde.StatusNotifierItem.IconPixmap property. +// +// Annotations: +// @org.qtproject.QtDBus.QtTypeName = KDbusImageVector +func (o *StatusNotifierItem) GetIconPixmap(ctx context.Context) (iconPixmap []struct { + V0 int32 + V1 int32 + V2 []byte +}, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "IconPixmap").Store(&iconPixmap) + return +} + +// GetOverlayIconName gets org.kde.StatusNotifierItem.OverlayIconName property. +func (o *StatusNotifierItem) GetOverlayIconName(ctx context.Context) (overlayIconName string, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "OverlayIconName").Store(&overlayIconName) + return +} + +// GetOverlayIconPixmap gets org.kde.StatusNotifierItem.OverlayIconPixmap property. +// +// Annotations: +// @org.qtproject.QtDBus.QtTypeName = KDbusImageVector +func (o *StatusNotifierItem) GetOverlayIconPixmap(ctx context.Context) (overlayIconPixmap []struct { + V0 int32 + V1 int32 + V2 []byte +}, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "OverlayIconPixmap").Store(&overlayIconPixmap) + return +} + +// GetAttentionIconName gets org.kde.StatusNotifierItem.AttentionIconName property. +func (o *StatusNotifierItem) GetAttentionIconName(ctx context.Context) (attentionIconName string, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "AttentionIconName").Store(&attentionIconName) + return +} + +// GetAttentionIconPixmap gets org.kde.StatusNotifierItem.AttentionIconPixmap property. +// +// Annotations: +// @org.qtproject.QtDBus.QtTypeName = KDbusImageVector +func (o *StatusNotifierItem) GetAttentionIconPixmap(ctx context.Context) (attentionIconPixmap []struct { + V0 int32 + V1 int32 + V2 []byte +}, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "AttentionIconPixmap").Store(&attentionIconPixmap) + return +} + +// GetAttentionMovieName gets org.kde.StatusNotifierItem.AttentionMovieName property. +func (o *StatusNotifierItem) GetAttentionMovieName(ctx context.Context) (attentionMovieName string, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "AttentionMovieName").Store(&attentionMovieName) + return +} + +// GetToolTip gets org.kde.StatusNotifierItem.ToolTip property. +// +// Annotations: +// @org.qtproject.QtDBus.QtTypeName = KDbusToolTipStruct +func (o *StatusNotifierItem) GetToolTip(ctx context.Context) (toolTip struct { + V0 string + V1 []struct { + V0 int32 + V1 int32 + V2 []byte + } + V2 string + V3 string +}, err error) { + err = o.object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, InterfaceStatusNotifierItem, "ToolTip").Store(&toolTip) + return +} + +// StatusNotifierItem_NewTitleSignal represents org.kde.StatusNotifierItem.NewTitle signal. +type StatusNotifierItem_NewTitleSignal struct { + sender string + Path dbus.ObjectPath + Body *StatusNotifierItem_NewTitleSignalBody +} + +// Name returns the signal's name. +func (s *StatusNotifierItem_NewTitleSignal) Name() string { + return "NewTitle" +} + +// Interface returns the signal's interface. +func (s *StatusNotifierItem_NewTitleSignal) Interface() string { + return InterfaceStatusNotifierItem +} + +// Sender returns the signal's sender unique name. +func (s *StatusNotifierItem_NewTitleSignal) Sender() string { + return s.sender +} + +func (s *StatusNotifierItem_NewTitleSignal) path() dbus.ObjectPath { + return s.Path +} + +func (s *StatusNotifierItem_NewTitleSignal) values() []interface{} { + return []interface{}{} +} + +// StatusNotifierItem_NewTitleSignalBody is body container. +type StatusNotifierItem_NewTitleSignalBody struct { +} + +// StatusNotifierItem_NewIconSignal represents org.kde.StatusNotifierItem.NewIcon signal. +type StatusNotifierItem_NewIconSignal struct { + sender string + Path dbus.ObjectPath + Body *StatusNotifierItem_NewIconSignalBody +} + +// Name returns the signal's name. +func (s *StatusNotifierItem_NewIconSignal) Name() string { + return "NewIcon" +} + +// Interface returns the signal's interface. +func (s *StatusNotifierItem_NewIconSignal) Interface() string { + return InterfaceStatusNotifierItem +} + +// Sender returns the signal's sender unique name. +func (s *StatusNotifierItem_NewIconSignal) Sender() string { + return s.sender +} + +func (s *StatusNotifierItem_NewIconSignal) path() dbus.ObjectPath { + return s.Path +} + +func (s *StatusNotifierItem_NewIconSignal) values() []interface{} { + return []interface{}{} +} + +// StatusNotifierItem_NewIconSignalBody is body container. +type StatusNotifierItem_NewIconSignalBody struct { +} + +// StatusNotifierItem_NewAttentionIconSignal represents org.kde.StatusNotifierItem.NewAttentionIcon signal. +type StatusNotifierItem_NewAttentionIconSignal struct { + sender string + Path dbus.ObjectPath + Body *StatusNotifierItem_NewAttentionIconSignalBody +} + +// Name returns the signal's name. +func (s *StatusNotifierItem_NewAttentionIconSignal) Name() string { + return "NewAttentionIcon" +} + +// Interface returns the signal's interface. +func (s *StatusNotifierItem_NewAttentionIconSignal) Interface() string { + return InterfaceStatusNotifierItem +} + +// Sender returns the signal's sender unique name. +func (s *StatusNotifierItem_NewAttentionIconSignal) Sender() string { + return s.sender +} + +func (s *StatusNotifierItem_NewAttentionIconSignal) path() dbus.ObjectPath { + return s.Path +} + +func (s *StatusNotifierItem_NewAttentionIconSignal) values() []interface{} { + return []interface{}{} +} + +// StatusNotifierItem_NewAttentionIconSignalBody is body container. +type StatusNotifierItem_NewAttentionIconSignalBody struct { +} + +// StatusNotifierItem_NewOverlayIconSignal represents org.kde.StatusNotifierItem.NewOverlayIcon signal. +type StatusNotifierItem_NewOverlayIconSignal struct { + sender string + Path dbus.ObjectPath + Body *StatusNotifierItem_NewOverlayIconSignalBody +} + +// Name returns the signal's name. +func (s *StatusNotifierItem_NewOverlayIconSignal) Name() string { + return "NewOverlayIcon" +} + +// Interface returns the signal's interface. +func (s *StatusNotifierItem_NewOverlayIconSignal) Interface() string { + return InterfaceStatusNotifierItem +} + +// Sender returns the signal's sender unique name. +func (s *StatusNotifierItem_NewOverlayIconSignal) Sender() string { + return s.sender +} + +func (s *StatusNotifierItem_NewOverlayIconSignal) path() dbus.ObjectPath { + return s.Path +} + +func (s *StatusNotifierItem_NewOverlayIconSignal) values() []interface{} { + return []interface{}{} +} + +// StatusNotifierItem_NewOverlayIconSignalBody is body container. +type StatusNotifierItem_NewOverlayIconSignalBody struct { +} + +// StatusNotifierItem_NewStatusSignal represents org.kde.StatusNotifierItem.NewStatus signal. +type StatusNotifierItem_NewStatusSignal struct { + sender string + Path dbus.ObjectPath + Body *StatusNotifierItem_NewStatusSignalBody +} + +// Name returns the signal's name. +func (s *StatusNotifierItem_NewStatusSignal) Name() string { + return "NewStatus" +} + +// Interface returns the signal's interface. +func (s *StatusNotifierItem_NewStatusSignal) Interface() string { + return InterfaceStatusNotifierItem +} + +// Sender returns the signal's sender unique name. +func (s *StatusNotifierItem_NewStatusSignal) Sender() string { + return s.sender +} + +func (s *StatusNotifierItem_NewStatusSignal) path() dbus.ObjectPath { + return s.Path +} + +func (s *StatusNotifierItem_NewStatusSignal) values() []interface{} { + return []interface{}{s.Body.Status} +} + +// StatusNotifierItem_NewStatusSignalBody is body container. +type StatusNotifierItem_NewStatusSignalBody struct { + Status string +} + +// StatusNotifierItem_NewIconThemePathSignal represents org.kde.StatusNotifierItem.NewIconThemePath signal. +type StatusNotifierItem_NewIconThemePathSignal struct { + sender string + Path dbus.ObjectPath + Body *StatusNotifierItem_NewIconThemePathSignalBody +} + +// Name returns the signal's name. +func (s *StatusNotifierItem_NewIconThemePathSignal) Name() string { + return "NewIconThemePath" +} + +// Interface returns the signal's interface. +func (s *StatusNotifierItem_NewIconThemePathSignal) Interface() string { + return InterfaceStatusNotifierItem +} + +// Sender returns the signal's sender unique name. +func (s *StatusNotifierItem_NewIconThemePathSignal) Sender() string { + return s.sender +} + +func (s *StatusNotifierItem_NewIconThemePathSignal) path() dbus.ObjectPath { + return s.Path +} + +func (s *StatusNotifierItem_NewIconThemePathSignal) values() []interface{} { + return []interface{}{s.Body.IconThemePath} +} + +// StatusNotifierItem_NewIconThemePathSignalBody is body container. +type StatusNotifierItem_NewIconThemePathSignalBody struct { + IconThemePath string +} + +// StatusNotifierItem_NewMenuSignal represents org.kde.StatusNotifierItem.NewMenu signal. +type StatusNotifierItem_NewMenuSignal struct { + sender string + Path dbus.ObjectPath + Body *StatusNotifierItem_NewMenuSignalBody +} + +// Name returns the signal's name. +func (s *StatusNotifierItem_NewMenuSignal) Name() string { + return "NewMenu" +} + +// Interface returns the signal's interface. +func (s *StatusNotifierItem_NewMenuSignal) Interface() string { + return InterfaceStatusNotifierItem +} + +// Sender returns the signal's sender unique name. +func (s *StatusNotifierItem_NewMenuSignal) Sender() string { + return s.sender +} + +func (s *StatusNotifierItem_NewMenuSignal) path() dbus.ObjectPath { + return s.Path +} + +func (s *StatusNotifierItem_NewMenuSignal) values() []interface{} { + return []interface{}{} +} + +// StatusNotifierItem_NewMenuSignalBody is body container. +type StatusNotifierItem_NewMenuSignalBody struct { +} diff --git a/vendor/fyne.io/systray/systray.go b/vendor/fyne.io/systray/systray.go new file mode 100644 index 0000000..9429eb5 --- /dev/null +++ b/vendor/fyne.io/systray/systray.go @@ -0,0 +1,308 @@ +// Package systray is a cross-platform Go library to place an icon and menu in the notification area. +package systray + +import ( + "fmt" + "log" + "runtime" + "sync" + "sync/atomic" +) + +var ( + systrayReady, systrayExit func() + tappedLeft, tappedRight func() + systrayExitCalled bool + menuItems = make(map[uint32]*MenuItem) + menuItemsLock sync.RWMutex + + currentID atomic.Uint32 + quitOnce sync.Once + + // TrayOpenedCh receives an entry each time the system tray menu is opened. + TrayOpenedCh = make(chan struct{}) +) + +// This helper function allows us to call systrayExit only once, +// without accidentally calling it twice in the same lifetime. +func runSystrayExit() { + if !systrayExitCalled { + systrayExitCalled = true + systrayExit() + } +} + +func init() { + runtime.LockOSThread() +} + +// MenuItem is used to keep track each menu item of systray. +// Don't create it directly, use the one systray.AddMenuItem() returned +type MenuItem struct { + // ClickedCh is the channel which will be notified when the menu item is clicked + ClickedCh chan struct{} + + // id uniquely identify a menu item, not supposed to be modified + id uint32 + // title is the text shown on menu item + title string + // tooltip is the text shown when pointing to menu item + tooltip string + // disabled menu item is grayed out and has no effect when clicked + disabled bool + // checked menu item has a tick before the title + checked bool + // has the menu item a checkbox (Linux) + isCheckable bool + // parent item, for sub menus + parent *MenuItem +} + +func (item *MenuItem) String() string { + if item.parent == nil { + return fmt.Sprintf("MenuItem[%d, %q]", item.id, item.title) + } + return fmt.Sprintf("MenuItem[%d, parent %d, %q]", item.id, item.parent.id, item.title) +} + +// newMenuItem returns a populated MenuItem object +func newMenuItem(title string, tooltip string, parent *MenuItem) *MenuItem { + return &MenuItem{ + ClickedCh: make(chan struct{}), + id: currentID.Add(1), + title: title, + tooltip: tooltip, + disabled: false, + checked: false, + isCheckable: false, + parent: parent, + } +} + +// Run initializes GUI and starts the event loop, then invokes the onReady +// callback. It blocks until systray.Quit() is called. +func Run(onReady, onExit func()) { + setInternalLoop(true) + Register(onReady, onExit) + + nativeLoop() +} + +// RunWithExternalLoop allows the system tray module to operate with other toolkits. +// The returned start and end functions should be called by the toolkit when the application has started and will end. +func RunWithExternalLoop(onReady, onExit func()) (start, end func()) { + Register(onReady, onExit) + + return nativeStart, func() { + nativeEnd() + Quit() + } +} + +// Register initializes GUI and registers the callbacks but relies on the +// caller to run the event loop somewhere else. It's useful if the program +// needs to show other UI elements, for example, webview. +// To overcome some OS weirdness, On macOS versions before Catalina, calling +// this does exactly the same as Run(). +func Register(onReady func(), onExit func()) { + if onReady == nil { + systrayReady = func() {} + } else { + // Run onReady on separate goroutine to avoid blocking event loop + readyCh := make(chan interface{}) + go func() { + <-readyCh + onReady() + }() + systrayReady = func() { + close(readyCh) + } + } + // unlike onReady, onExit runs in the event loop to make sure it has time to + // finish before the process terminates + if onExit == nil { + onExit = func() {} + } + systrayExit = onExit + systrayExitCalled = false + registerSystray() +} + +// ResetMenu will remove all menu items +func ResetMenu() { + menuItemsLock.Lock() + id := currentID.Load() + menuItemsLock.Unlock() + for i, item := range menuItems { + if i < id && item.parent == nil { + item.Remove() + } + } + resetMenu() +} + +// Quit the systray +func Quit() { + quitOnce.Do(quit) +} + +func SetOnTapped(f func()) { + tappedLeft = f +} + +func SetOnSecondaryTapped(f func()) { + tappedRight = f +} + +// AddMenuItem adds a menu item with the designated title and tooltip. +// It can be safely invoked from different goroutines. +// Created menu items are checkable on Windows and OSX by default. For Linux you have to use AddMenuItemCheckbox +func AddMenuItem(title string, tooltip string) *MenuItem { + item := newMenuItem(title, tooltip, nil) + item.update() + return item +} + +// AddMenuItemCheckbox adds a menu item with the designated title and tooltip and a checkbox for Linux. +// On other platforms there will be a check indicated next to the item if `checked` is true. +// It can be safely invoked from different goroutines. +func AddMenuItemCheckbox(title string, tooltip string, checked bool) *MenuItem { + item := newMenuItem(title, tooltip, nil) + item.isCheckable = true + item.checked = checked + item.update() + return item +} + +// AddSeparator adds a separator bar to the menu +func AddSeparator() { + addSeparator(currentID.Add(1), 0) +} + +// AddSeparator adds a separator bar to the submenu +func (item *MenuItem) AddSeparator() { + addSeparator(currentID.Add(1), item.id) +} + +// AddSubMenuItem adds a nested sub-menu item with the designated title and tooltip. +// It can be safely invoked from different goroutines. +// Created menu items are checkable on Windows and OSX by default. For Linux you have to use AddSubMenuItemCheckbox +func (item *MenuItem) AddSubMenuItem(title string, tooltip string) *MenuItem { + child := newMenuItem(title, tooltip, item) + child.update() + return child +} + +// AddSubMenuItemCheckbox adds a nested sub-menu item with the designated title and tooltip and a checkbox for Linux. +// It can be safely invoked from different goroutines. +// On Windows and OSX this is the same as calling AddSubMenuItem +func (item *MenuItem) AddSubMenuItemCheckbox(title string, tooltip string, checked bool) *MenuItem { + child := newMenuItem(title, tooltip, item) + child.isCheckable = true + child.checked = checked + child.update() + return child +} + +// SetTitle set the text to display on a menu item +func (item *MenuItem) SetTitle(title string) { + item.title = title + item.update() +} + +// SetTooltip set the tooltip to show when mouse hover +func (item *MenuItem) SetTooltip(tooltip string) { + item.tooltip = tooltip + item.update() +} + +// Disabled checks if the menu item is disabled +func (item *MenuItem) Disabled() bool { + return item.disabled +} + +// Enable a menu item regardless if it's previously enabled or not +func (item *MenuItem) Enable() { + item.disabled = false + item.update() +} + +// Disable a menu item regardless if it's previously disabled or not +func (item *MenuItem) Disable() { + item.disabled = true + item.update() +} + +// Hide hides a menu item +func (item *MenuItem) Hide() { + hideMenuItem(item) +} + +// Remove removes a menu item +func (item *MenuItem) Remove() { + menuItemsLock.RLock() + var childList []*MenuItem + for _, child := range menuItems { + if child.parent == item { + childList = append(childList, child) + } + } + menuItemsLock.RUnlock() + for _, child := range childList { + child.Remove() + } + removeMenuItem(item) + menuItemsLock.Lock() + delete(menuItems, item.id) + select { + case <-item.ClickedCh: + default: + } + close(item.ClickedCh) + menuItemsLock.Unlock() +} + +// Show shows a previously hidden menu item +func (item *MenuItem) Show() { + showMenuItem(item) +} + +// Checked returns if the menu item has a check mark +func (item *MenuItem) Checked() bool { + return item.checked +} + +// Check a menu item regardless if it's previously checked or not +func (item *MenuItem) Check() { + item.checked = true + item.update() +} + +// Uncheck a menu item regardless if it's previously unchecked or not +func (item *MenuItem) Uncheck() { + item.checked = false + item.update() +} + +// update propagates changes on a menu item to systray +func (item *MenuItem) update() { + menuItemsLock.Lock() + menuItems[item.id] = item + menuItemsLock.Unlock() + addOrUpdateMenuItem(item) +} + +func systrayMenuItemSelected(id uint32) { + menuItemsLock.RLock() + item, ok := menuItems[id] + menuItemsLock.RUnlock() + if !ok { + log.Printf("systray error: no menu item with ID %d\n", id) + return + } + select { + case item.ClickedCh <- struct{}{}: + // in case no one waiting for the channel + default: + } +} diff --git a/vendor/fyne.io/systray/systray.h b/vendor/fyne.io/systray/systray.h new file mode 100644 index 0000000..96200bd --- /dev/null +++ b/vendor/fyne.io/systray/systray.h @@ -0,0 +1,26 @@ +#include "stdbool.h" + +extern void systray_ready(); +extern void systray_on_exit(); +extern void systray_left_click(); +extern void systray_right_click(); +extern void systray_menu_item_selected(int menu_id); +extern void systray_menu_will_open(); +void registerSystray(void); +void nativeEnd(void); +int nativeLoop(void); +void nativeStart(void); + +void setIcon(const char* iconBytes, int length, bool template); +void setMenuItemIcon(const char* iconBytes, int length, int menuId, bool template); +void setTitle(char* title); +void setTooltip(char* tooltip); +void setRemovalAllowed(bool allowed); +void add_or_update_menu_item(int menuId, int parentMenuId, char* title, char* tooltip, short disabled, short checked, short isCheckable); +void add_separator(int menuId, int parentId); +void hide_menu_item(int menuId); +void remove_menu_item(int menuId); +void show_menu_item(int menuId); +void reset_menu(); +void show_menu(); +void quit(); diff --git a/vendor/fyne.io/systray/systray_darwin.go b/vendor/fyne.io/systray/systray_darwin.go new file mode 100644 index 0000000..612e75a --- /dev/null +++ b/vendor/fyne.io/systray/systray_darwin.go @@ -0,0 +1,213 @@ +//go:build !ios + +package systray + +/* +#cgo darwin CFLAGS: -DDARWIN -x objective-c -fobjc-arc +#cgo darwin LDFLAGS: -framework Cocoa + +#include +#include "systray.h" + +void setInternalLoop(bool); +*/ +import "C" + +import ( + "fmt" + "os" + "unsafe" +) + +// SetTemplateIcon sets the systray icon as a template icon (on Mac), falling back +// to a regular icon on other platforms. +// templateIconBytes and regularIconBytes should be the content of .ico for windows and +// .ico/.jpg/.png for other platforms. +func SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) { + cstr := (*C.char)(unsafe.Pointer(&templateIconBytes[0])) + C.setIcon(cstr, (C.int)(len(templateIconBytes)), true) +} + +// SetIcon sets the icon of a menu item. Only works on macOS and Windows. +// iconBytes should be the content of .ico/.jpg/.png +func (item *MenuItem) SetIcon(iconBytes []byte) { + cstr := (*C.char)(unsafe.Pointer(&iconBytes[0])) + C.setMenuItemIcon(cstr, (C.int)(len(iconBytes)), C.int(item.id), false) +} + +// SetIconFromFilePath sets the icon of a menu item from a file path. +// iconFilePath should be the path to a .ico for windows and .ico/.jpg/.png for other platforms. +func (item *MenuItem) SetIconFromFilePath(iconFilePath string) error { + iconBytes, err := os.ReadFile(iconFilePath) + if err != nil { + return fmt.Errorf("failed to read icon file: %v", err) + } + item.SetIcon(iconBytes) + return nil +} + +// SetTemplateIcon sets the icon of a menu item as a template icon (on macOS). On Windows, it +// falls back to the regular icon bytes and on Linux it does nothing. +// templateIconBytes and regularIconBytes should be the content of .ico for windows and +// .ico/.jpg/.png for other platforms. +func (item *MenuItem) SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) { + cstr := (*C.char)(unsafe.Pointer(&templateIconBytes[0])) + C.setMenuItemIcon(cstr, (C.int)(len(templateIconBytes)), C.int(item.id), true) +} + +// SetRemovalAllowed sets whether a user can remove the systray icon or not. +// This is only supported on macOS. +func SetRemovalAllowed(allowed bool) { + C.setRemovalAllowed((C.bool)(allowed)) +} + +func registerSystray() { + C.registerSystray() +} + +func nativeLoop() { + C.nativeLoop() +} + +func nativeEnd() { + C.nativeEnd() +} + +func nativeStart() { + C.nativeStart() +} + +func quit() { + C.quit() +} + +func setInternalLoop(internal bool) { + C.setInternalLoop(C.bool(internal)) +} + +// SetIcon sets the systray icon. +// iconBytes should be the content of .ico for windows and .ico/.jpg/.png +// for other platforms. +func SetIcon(iconBytes []byte) { + cstr := (*C.char)(unsafe.Pointer(&iconBytes[0])) + C.setIcon(cstr, (C.int)(len(iconBytes)), false) +} + +// SetIconFromFilePath sets the systray icon from a file path. +// iconFilePath should be the path to a .ico for windows and .ico/.jpg/.png for other platforms. +func SetIconFromFilePath(iconFilePath string) error { + bytes, err := os.ReadFile(iconFilePath) + if err != nil { + return fmt.Errorf("failed to read icon file: %v", err) + } + SetIcon(bytes) + return nil +} + +// SetTitle sets the systray title, only available on Mac and Linux. +func SetTitle(title string) { + C.setTitle(C.CString(title)) +} + +// SetTooltip sets the systray tooltip to display on mouse hover of the tray icon, +// only available on Mac and Windows. +func SetTooltip(tooltip string) { + C.setTooltip(C.CString(tooltip)) +} + +func addOrUpdateMenuItem(item *MenuItem) { + var disabled C.short + if item.disabled { + disabled = 1 + } + var checked C.short + if item.checked { + checked = 1 + } + var isCheckable C.short + if item.isCheckable { + isCheckable = 1 + } + var parentID uint32 = 0 + if item.parent != nil { + parentID = item.parent.id + } + C.add_or_update_menu_item( + C.int(item.id), + C.int(parentID), + C.CString(item.title), + C.CString(item.tooltip), + disabled, + checked, + isCheckable, + ) +} + +func addSeparator(id uint32, parent uint32) { + C.add_separator(C.int(id), C.int(parent)) +} + +func hideMenuItem(item *MenuItem) { + C.hide_menu_item( + C.int(item.id), + ) +} + +func showMenuItem(item *MenuItem) { + C.show_menu_item( + C.int(item.id), + ) +} + +func removeMenuItem(item *MenuItem) { + C.remove_menu_item( + C.int(item.id), + ) +} + +func resetMenu() { + C.reset_menu() +} + +//export systray_left_click +func systray_left_click() { + if fn := tappedLeft; fn != nil { + fn() + return + } + + C.show_menu() +} + +//export systray_right_click +func systray_right_click() { + if fn := tappedRight; fn != nil { + fn() + return + } + + C.show_menu() +} + +//export systray_ready +func systray_ready() { + systrayReady() +} + +//export systray_on_exit +func systray_on_exit() { + runSystrayExit() +} + +//export systray_menu_item_selected +func systray_menu_item_selected(cID C.int) { + systrayMenuItemSelected(uint32(cID)) +} + +//export systray_menu_will_open +func systray_menu_will_open() { + select { + case TrayOpenedCh <- struct{}{}: + default: + } +} diff --git a/vendor/fyne.io/systray/systray_darwin.m b/vendor/fyne.io/systray/systray_darwin.m new file mode 100644 index 0000000..b0e9ee8 --- /dev/null +++ b/vendor/fyne.io/systray/systray_darwin.m @@ -0,0 +1,464 @@ +//go:build !ios + +#import +#include "systray.h" + +#if __MAC_OS_X_VERSION_MIN_REQUIRED < 101400 + + #ifndef NSControlStateValueOff + #define NSControlStateValueOff NSOffState + #endif + + #ifndef NSControlStateValueOn + #define NSControlStateValueOn NSOnState + #endif + +#endif + +@interface MenuItem : NSObject +{ + @public + NSNumber* menuId; + NSNumber* parentMenuId; + NSString* title; + NSString* tooltip; + short disabled; + short checked; +} +-(id) initWithId: (int)theMenuId +withParentMenuId: (int)theParentMenuId + withTitle: (const char*)theTitle + withTooltip: (const char*)theTooltip + withDisabled: (short)theDisabled + withChecked: (short)theChecked; + @end + @implementation MenuItem + -(id) initWithId: (int)theMenuId + withParentMenuId: (int)theParentMenuId + withTitle: (const char*)theTitle + withTooltip: (const char*)theTooltip + withDisabled: (short)theDisabled + withChecked: (short)theChecked +{ + menuId = [NSNumber numberWithInt:theMenuId]; + parentMenuId = [NSNumber numberWithInt:theParentMenuId]; + title = [[NSString alloc] initWithCString:theTitle + encoding:NSUTF8StringEncoding]; + tooltip = [[NSString alloc] initWithCString:theTooltip + encoding:NSUTF8StringEncoding]; + disabled = theDisabled; + checked = theChecked; + return self; +} +@end + +@interface RightClickDetector : NSView + +@property (copy) void (^onRightClicked)(NSEvent *); + +@end + +@implementation RightClickDetector + +- (void)rightMouseUp:(NSEvent *)theEvent { + if (!self.onRightClicked) { + return; + } + + self.onRightClicked(theEvent); +} + +@end + + +@interface SystrayAppDelegate: NSObject + - (void) add_or_update_menu_item:(MenuItem*) item; + - (IBAction)menuHandler:(id)sender; + - (void)menuWillOpen:(NSMenu*)menu; + @property (assign) IBOutlet NSWindow *window; +@end + +@implementation SystrayAppDelegate +{ + NSStatusItem *statusItem; + NSMenu *menu; + NSCondition* cond; +} + +@synthesize window = _window; + +- (void)applicationDidFinishLaunching:(NSNotification *)aNotification +{ + self->statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength]; + + self->menu = [[NSMenu alloc] init]; + self->menu.delegate = self; + self->menu.autoenablesItems = FALSE; + // Once the user has removed it, the item needs to be explicitly brought back, + // even restarting the application is insufficient. + // Since the interface from Go is relatively simple, for now we ensure it's + // always visible at application startup. + self->statusItem.visible = TRUE; + + NSStatusBarButton *button = self->statusItem.button; + button.action = @selector(leftMouseClicked); + + [NSEvent addLocalMonitorForEventsMatchingMask: (NSEventTypeLeftMouseDown|NSEventTypeRightMouseDown) + handler: ^NSEvent *(NSEvent *event) { + if (event.window != self->statusItem.button.window) { + return event; + } + + [self leftMouseClicked]; + + return nil; + }]; + + NSSize size = [button frame].size; + NSRect frame = CGRectMake(0, 0, size.width, size.height); + RightClickDetector *rightClicker = [[RightClickDetector alloc] initWithFrame:frame]; + rightClicker.onRightClicked = ^(NSEvent *event) { + [self rightMouseClicked]; + }; + + rightClicker.autoresizingMask = (NSViewWidthSizable | + NSViewHeightSizable); + button.autoresizesSubviews = YES; + [button addSubview:rightClicker]; + + systray_ready(); +} + +- (void)rightMouseClicked { + systray_right_click(); +} + +- (void)leftMouseClicked { + systray_left_click(); +} + +- (void)applicationWillTerminate:(NSNotification *)aNotification +{ + systray_on_exit(); +} + +- (void)setRemovalAllowed { + NSStatusItemBehavior behavior = [self->statusItem behavior]; + behavior |= NSStatusItemBehaviorRemovalAllowed; + self->statusItem.behavior = behavior; +} + +- (void)setRemovalForbidden { + NSStatusItemBehavior behavior = [self->statusItem behavior]; + behavior &= ~NSStatusItemBehaviorRemovalAllowed; + // Ensure the menu item is visible if it was removed, since we're now + // disallowing removal. + self->statusItem.visible = TRUE; + self->statusItem.behavior = behavior; +} + +- (void)setIcon:(NSImage *)image { + statusItem.button.image = image; + [self updateTitleButtonStyle]; +} + +- (void)setTitle:(NSString *)title { + statusItem.button.title = title; + [self updateTitleButtonStyle]; +} + +- (void)updateTitleButtonStyle { + if (statusItem.button.image != nil) { + if ([statusItem.button.title length] == 0) { + statusItem.button.imagePosition = NSImageOnly; + } else { + statusItem.button.imagePosition = NSImageLeft; + } + } else { + statusItem.button.imagePosition = NSNoImage; + } +} + + +- (void)setTooltip:(NSString *)tooltip { + statusItem.button.toolTip = tooltip; +} + +- (IBAction)menuHandler:(id)sender +{ + NSNumber* menuId = [sender representedObject]; + systray_menu_item_selected(menuId.intValue); +} + +- (void)menuWillOpen:(NSMenu *)menu { + systray_menu_will_open(); +} + +- (void)add_or_update_menu_item:(MenuItem *)item { + NSMenu *theMenu = self->menu; + NSMenuItem *parentItem; + if ([item->parentMenuId integerValue] > 0) { + parentItem = find_menu_item(menu, item->parentMenuId); + if (parentItem.hasSubmenu) { + theMenu = parentItem.submenu; + } else { + theMenu = [[NSMenu alloc] init]; + [theMenu setAutoenablesItems:NO]; + [parentItem setSubmenu:theMenu]; + } + } + + NSMenuItem *menuItem = find_menu_item(theMenu, item->menuId); + if (menuItem == NULL) { + menuItem = [theMenu addItemWithTitle:item->title + action:@selector(menuHandler:) + keyEquivalent:@""]; + [menuItem setRepresentedObject:item->menuId]; + } + [menuItem setTitle:item->title]; + [menuItem setTag:[item->menuId integerValue]]; + [menuItem setTarget:self]; + [menuItem setToolTip:item->tooltip]; + if (item->disabled == 1) { + menuItem.enabled = FALSE; + } else { + menuItem.enabled = TRUE; + } + if (item->checked == 1) { + menuItem.state = NSControlStateValueOn; + } else { + menuItem.state = NSControlStateValueOff; + } +} + +NSMenuItem *find_menu_item(NSMenu *ourMenu, NSNumber *menuId) { + NSMenuItem *foundItem = [ourMenu itemWithTag:[menuId integerValue]]; + if (foundItem != NULL) { + return foundItem; + } + NSArray *menu_items = ourMenu.itemArray; + int i; + for (i = 0; i < [menu_items count]; i++) { + NSMenuItem *i_item = [menu_items objectAtIndex:i]; + if (i_item.hasSubmenu) { + foundItem = find_menu_item(i_item.submenu, menuId); + if (foundItem != NULL) { + return foundItem; + } + } + } + + return NULL; +}; + +- (void) add_separator:(NSNumber*) parentMenuId +{ + if (parentMenuId.integerValue != 0) { + NSMenuItem* menuItem = find_menu_item(menu, parentMenuId); + if (menuItem != NULL) { + [menuItem.submenu addItem: [NSMenuItem separatorItem]]; + return; + } + } + [menu addItem: [NSMenuItem separatorItem]]; +} + +- (void) hide_menu_item:(NSNumber*) menuId +{ + NSMenuItem* menuItem = find_menu_item(menu, menuId); + if (menuItem != NULL) { + [menuItem setHidden:TRUE]; + } +} + +- (void) setMenuItemIcon:(NSArray*)imageAndMenuId { + NSImage* image = [imageAndMenuId objectAtIndex:0]; + NSNumber* menuId = [imageAndMenuId objectAtIndex:1]; + + NSMenuItem* menuItem; + menuItem = find_menu_item(menu, menuId); + if (menuItem == NULL) { + return; + } + menuItem.image = image; +} + +- (void)show_menu +{ + [self->menu popUpMenuPositioningItem:nil + atLocation:NSMakePoint(0, self->statusItem.button.bounds.size.height+6) + inView:self->statusItem.button]; +} + +- (void) show_menu_item:(NSNumber*) menuId +{ + NSMenuItem* menuItem = find_menu_item(menu, menuId); + if (menuItem != NULL) { + [menuItem setHidden:FALSE]; + } +} + +- (void) remove_menu_item:(NSNumber*) menuId +{ + NSMenuItem* menuItem = find_menu_item(menu, menuId); + if (menuItem != NULL) { + [menuItem.menu removeItem:menuItem]; + } +} + +- (void) reset_menu +{ + [self->menu removeAllItems]; +} + +- (void) quit +{ + // This tells the app event loop to stop after processing remaining messages. + [NSApp stop:self]; + // The event loop won't return until it processes another event. + // https://stackoverflow.com/a/48064752/149482 + NSPoint eventLocation = NSMakePoint(0, 0); + NSEvent *customEvent = [NSEvent otherEventWithType:NSEventTypeApplicationDefined + location:eventLocation + modifierFlags:0 + timestamp:0 + windowNumber:0 + context:nil + subtype:0 + data1:0 + data2:0]; + [NSApp postEvent:customEvent atStart:NO]; +} + +@end + +bool internalLoop = false; +SystrayAppDelegate *owner; + +void setInternalLoop(bool i) { + internalLoop = i; +} + +void registerSystray(void) { + if (!internalLoop) { // with an external loop we don't take ownership of the app + return; + } + + owner = [[SystrayAppDelegate alloc] init]; + [[NSApplication sharedApplication] setDelegate:owner]; + + // A workaround to avoid crashing on macOS versions before Catalina. Somehow + // SIGSEGV would happen inside AppKit if [NSApp run] is called from a + // different function, even if that function is called right after this. + if (floor(NSAppKitVersionNumber) <= /*NSAppKitVersionNumber10_14*/ 1671){ + [NSApp run]; + } +} + +void nativeEnd(void) { + systray_on_exit(); +} + +int nativeLoop(void) { + if (floor(NSAppKitVersionNumber) > /*NSAppKitVersionNumber10_14*/ 1671){ + [NSApp run]; + } + return EXIT_SUCCESS; +} + +void nativeStart(void) { + owner = [[SystrayAppDelegate alloc] init]; + + NSNotification *launched = [NSNotification notificationWithName:NSApplicationDidFinishLaunchingNotification + object:[NSApplication sharedApplication]]; + [owner applicationDidFinishLaunching:launched]; +} + +void runInMainThread(SEL method, id object) { + [owner + performSelectorOnMainThread:method + withObject:object + waitUntilDone: YES]; +} + +void setIcon(const char* iconBytes, int length, bool template) { + NSData* buffer = [NSData dataWithBytes: iconBytes length:length]; + @autoreleasepool { + NSImage *image = [[NSImage alloc] initWithData:buffer]; + [image setSize:NSMakeSize(16, 16)]; + image.template = template; + runInMainThread(@selector(setIcon:), (id)image); + } +} + +void setMenuItemIcon(const char* iconBytes, int length, int menuId, bool template) { + NSData* buffer = [NSData dataWithBytes: iconBytes length:length]; + @autoreleasepool { + NSImage *image = [[NSImage alloc] initWithData:buffer]; + [image setSize:NSMakeSize(16, 16)]; + image.template = template; + NSNumber *mId = [NSNumber numberWithInt:menuId]; + runInMainThread(@selector(setMenuItemIcon:), @[image, (id)mId]); + } +} + +void setTitle(char* ctitle) { + NSString* title = [[NSString alloc] initWithCString:ctitle + encoding:NSUTF8StringEncoding]; + free(ctitle); + runInMainThread(@selector(setTitle:), (id)title); +} + +void setTooltip(char* ctooltip) { + NSString* tooltip = [[NSString alloc] initWithCString:ctooltip + encoding:NSUTF8StringEncoding]; + free(ctooltip); + runInMainThread(@selector(setTooltip:), (id)tooltip); +} + +void setRemovalAllowed(bool allowed) { + if (allowed) { + runInMainThread(@selector(setRemovalAllowed), nil); + } else { + runInMainThread(@selector(setRemovalForbidden), nil); + } +} + +void add_or_update_menu_item(int menuId, int parentMenuId, char* title, char* tooltip, short disabled, short checked, short isCheckable) { + MenuItem* item = [[MenuItem alloc] initWithId: menuId withParentMenuId: parentMenuId withTitle: title withTooltip: tooltip withDisabled: disabled withChecked: checked]; + free(title); + free(tooltip); + runInMainThread(@selector(add_or_update_menu_item:), (id)item); +} + +void add_separator(int menuId, int parentId) { + NSNumber *pId = [NSNumber numberWithInt:parentId]; + runInMainThread(@selector(add_separator:), (id)pId); +} + +void hide_menu_item(int menuId) { + NSNumber *mId = [NSNumber numberWithInt:menuId]; + runInMainThread(@selector(hide_menu_item:), (id)mId); +} + +void remove_menu_item(int menuId) { + NSNumber *mId = [NSNumber numberWithInt:menuId]; + runInMainThread(@selector(remove_menu_item:), (id)mId); +} + +void show_menu() { + runInMainThread(@selector(show_menu), nil); +} + +void show_menu_item(int menuId) { + NSNumber *mId = [NSNumber numberWithInt:menuId]; + runInMainThread(@selector(show_menu_item:), (id)mId); +} + +void reset_menu() { + runInMainThread(@selector(reset_menu), nil); +} + +void quit() { + runInMainThread(@selector(quit), nil); +} diff --git a/vendor/fyne.io/systray/systray_menu_unix.go b/vendor/fyne.io/systray/systray_menu_unix.go new file mode 100644 index 0000000..283ce31 --- /dev/null +++ b/vendor/fyne.io/systray/systray_menu_unix.go @@ -0,0 +1,367 @@ +//go:build (linux || freebsd || openbsd || netbsd) && !android + +package systray + +import ( + "fmt" + "log" + "os" + + "github.com/godbus/dbus/v5" + "github.com/godbus/dbus/v5/prop" + + "fyne.io/systray/internal/generated/menu" +) + +// SetIcon sets the icon of a menu item. +// iconBytes should be the content of .ico/.jpg/.png +func (item *MenuItem) SetIcon(iconBytes []byte) { + instance.menuLock.Lock() + defer instance.menuLock.Unlock() + m, exists := findLayout(int32(item.id)) + if exists { + m.V1["icon-data"] = dbus.MakeVariant(iconBytes) + refresh() + } +} + +// SetIconFromFilePath sets the icon of a menu item from a file path. +// iconFilePath should be the path to a .ico for windows and .ico/.jpg/.png for other platforms. +func (item *MenuItem) SetIconFromFilePath(iconFilePath string) error { + iconBytes, err := os.ReadFile(iconFilePath) + if err != nil { + return fmt.Errorf("failed to read icon file: %v", err) + } + item.SetIcon(iconBytes) + return nil +} + +// copyLayout makes full copy of layout +func copyLayout(in *menuLayout, depth int32) *menuLayout { + out := menuLayout{ + V0: in.V0, + V1: make(map[string]dbus.Variant, len(in.V1)), + } + for k, v := range in.V1 { + out.V1[k] = v + } + if depth != 0 { + depth-- + out.V2 = make([]dbus.Variant, len(in.V2)) + for i, v := range in.V2 { + out.V2[i] = dbus.MakeVariant(copyLayout(v.Value().(*menuLayout), depth)) + } + } else { + out.V2 = []dbus.Variant{} + } + return &out +} + +// GetLayout is com.canonical.dbusmenu.GetLayout method. +func (t *tray) GetLayout(parentID int32, recursionDepth int32, propertyNames []string) (revision uint32, layout menuLayout, err *dbus.Error) { + instance.menuLock.Lock() + defer instance.menuLock.Unlock() + if m, ok := findLayout(parentID); ok { + // return copy of menu layout to prevent panic from cuncurrent access to layout + return instance.menuVersion, *copyLayout(m, recursionDepth), nil + } + return +} + +// GetGroupProperties is com.canonical.dbusmenu.GetGroupProperties method. +func (t *tray) GetGroupProperties(ids []int32, propertyNames []string) (properties []struct { + V0 int32 + V1 map[string]dbus.Variant +}, err *dbus.Error) { + instance.menuLock.Lock() + defer instance.menuLock.Unlock() + for _, id := range ids { + if m, ok := findLayout(id); ok { + p := struct { + V0 int32 + V1 map[string]dbus.Variant + }{ + V0: m.V0, + V1: make(map[string]dbus.Variant, len(m.V1)), + } + for k, v := range m.V1 { + p.V1[k] = v + } + properties = append(properties, p) + } + } + return +} + +// GetProperty is com.canonical.dbusmenu.GetProperty method. +func (t *tray) GetProperty(id int32, name string) (value dbus.Variant, err *dbus.Error) { + instance.menuLock.Lock() + defer instance.menuLock.Unlock() + if m, ok := findLayout(id); ok { + if p, ok := m.V1[name]; ok { + return p, nil + } + } + return +} + +// Event is com.canonical.dbusmenu.Event method. +func (t *tray) Event(id int32, eventID string, data dbus.Variant, timestamp uint32) (err *dbus.Error) { + switch eventID { + case "clicked": + systrayMenuItemSelected(uint32(id)) + case "opened": + t.menuLock.RLock() + rootMenuID := t.menu.V0 + t.menuLock.RUnlock() + + if id == rootMenuID { + select { + case TrayOpenedCh <- struct{}{}: + default: + } + } + } + return +} + +// EventGroup is com.canonical.dbusmenu.EventGroup method. +func (t *tray) EventGroup(events []struct { + V0 int32 + V1 string + V2 dbus.Variant + V3 uint32 +}) (idErrors []int32, err *dbus.Error) { + for _, event := range events { + if event.V1 == "clicked" { + systrayMenuItemSelected(uint32(event.V0)) + } + } + return +} + +// AboutToShow is com.canonical.dbusmenu.AboutToShow method. +func (t *tray) AboutToShow(id int32) (needUpdate bool, err *dbus.Error) { + return +} + +// AboutToShowGroup is com.canonical.dbusmenu.AboutToShowGroup method. +func (t *tray) AboutToShowGroup(ids []int32) (updatesNeeded []int32, idErrors []int32, err *dbus.Error) { + return +} + +func createMenuPropSpec() map[string]map[string]*prop.Prop { + instance.menuLock.Lock() + defer instance.menuLock.Unlock() + return map[string]map[string]*prop.Prop{ + "com.canonical.dbusmenu": { + "Version": { + Value: instance.menuVersion, + Writable: true, + Emit: prop.EmitTrue, + Callback: nil, + }, + "TextDirection": { + Value: "ltr", + Writable: false, + Emit: prop.EmitTrue, + Callback: nil, + }, + "Status": { + Value: "normal", + Writable: false, + Emit: prop.EmitTrue, + Callback: nil, + }, + "IconThemePath": { + Value: []string{}, + Writable: false, + Emit: prop.EmitTrue, + Callback: nil, + }, + }, + } +} + +// menuLayout is a named struct to map into generated bindings. It represents the layout of a menu item +type menuLayout = struct { + V0 int32 // the unique ID of this item + V1 map[string]dbus.Variant // properties for this menu item layout + V2 []dbus.Variant // child menu item layouts +} + +func addOrUpdateMenuItem(item *MenuItem) { + var layout *menuLayout + instance.menuLock.Lock() + defer instance.menuLock.Unlock() + m, exists := findLayout(int32(item.id)) + if exists { + layout = m + } else { + layout = &menuLayout{ + V0: int32(item.id), + V1: map[string]dbus.Variant{}, + V2: []dbus.Variant{}, + } + + parent := instance.menu + if item.parent != nil { + m, ok := findLayout(int32(item.parent.id)) + if ok { + parent = m + parent.V1["children-display"] = dbus.MakeVariant("submenu") + } + } + parent.V2 = append(parent.V2, dbus.MakeVariant(layout)) + } + + applyItemToLayout(item, layout) + if exists { + refresh() + } +} + +func addSeparator(id uint32, parent uint32) { + menu, _ := findLayout(int32(parent)) + + instance.menuLock.Lock() + defer instance.menuLock.Unlock() + layout := &menuLayout{ + V0: int32(id), + V1: map[string]dbus.Variant{ + "type": dbus.MakeVariant("separator"), + }, + V2: []dbus.Variant{}, + } + menu.V2 = append(menu.V2, dbus.MakeVariant(layout)) + refresh() +} + +func applyItemToLayout(in *MenuItem, out *menuLayout) { + out.V1["enabled"] = dbus.MakeVariant(!in.disabled) + out.V1["label"] = dbus.MakeVariant(in.title) + + if in.isCheckable { + out.V1["toggle-type"] = dbus.MakeVariant("checkmark") + if in.checked { + out.V1["toggle-state"] = dbus.MakeVariant(1) + } else { + out.V1["toggle-state"] = dbus.MakeVariant(0) + } + } else { + out.V1["toggle-type"] = dbus.MakeVariant("") + out.V1["toggle-state"] = dbus.MakeVariant(0) + } +} + +func findLayout(id int32) (*menuLayout, bool) { + if id == 0 { + return instance.menu, true + } + return findSubLayout(id, instance.menu.V2) +} + +func findSubLayout(id int32, vals []dbus.Variant) (*menuLayout, bool) { + for _, i := range vals { + item := i.Value().(*menuLayout) + if item.V0 == id { + return item, true + } + + if len(item.V2) > 0 { + child, ok := findSubLayout(id, item.V2) + if ok { + return child, true + } + } + } + + return nil, false +} + +func removeSubLayout(id int32, vals []dbus.Variant) ([]dbus.Variant, bool) { + for idx, i := range vals { + item := i.Value().(*menuLayout) + if item.V0 == id { + return append(vals[:idx], vals[idx+1:]...), true + } + + if len(item.V2) > 0 { + if child, removed := removeSubLayout(id, item.V2); removed { + return child, true + } + } + } + + return vals, false +} + +func removeMenuItem(item *MenuItem) { + instance.menuLock.Lock() + defer instance.menuLock.Unlock() + + parent := instance.menu + if item.parent != nil { + m, ok := findLayout(int32(item.parent.id)) + if !ok { + return + } + parent = m + } + + if items, removed := removeSubLayout(int32(item.id), parent.V2); removed { + parent.V2 = items + refresh() + } +} + +func hideMenuItem(item *MenuItem) { + instance.menuLock.Lock() + defer instance.menuLock.Unlock() + m, exists := findLayout(int32(item.id)) + if exists { + m.V1["visible"] = dbus.MakeVariant(false) + refresh() + } +} + +func showMenuItem(item *MenuItem) { + instance.menuLock.Lock() + defer instance.menuLock.Unlock() + m, exists := findLayout(int32(item.id)) + if exists { + m.V1["visible"] = dbus.MakeVariant(true) + refresh() + } +} + +func refresh() { + if instance.conn == nil || instance.menuProps == nil { + return + } + instance.menuVersion++ + dbusErr := instance.menuProps.Set("com.canonical.dbusmenu", "Version", + dbus.MakeVariant(instance.menuVersion)) + if dbusErr != nil { + log.Printf("systray error: failed to update menu version: %v\n", dbusErr) + return + } + err := menu.Emit(instance.conn, &menu.Dbusmenu_LayoutUpdatedSignal{ + Path: menuPath, + Body: &menu.Dbusmenu_LayoutUpdatedSignalBody{ + Revision: instance.menuVersion, + }, + }) + if err != nil { + log.Printf("systray error: failed to emit layout updated signal: %v\n", err) + } + +} + +func resetMenu() { + instance.menuLock.Lock() + defer instance.menuLock.Unlock() + instance.menu = &menuLayout{} + instance.menuVersion++ + refresh() +} diff --git a/vendor/fyne.io/systray/systray_notifier_unix.go b/vendor/fyne.io/systray/systray_notifier_unix.go new file mode 100644 index 0000000..eb84493 --- /dev/null +++ b/vendor/fyne.io/systray/systray_notifier_unix.go @@ -0,0 +1,44 @@ +package systray + +import ( + "fyne.io/systray/internal/generated/notifier" + "github.com/godbus/dbus/v5" +) + +type leftRightNotifierItem struct { +} + +func newLeftRightNotifierItem() notifier.StatusNotifierItemer { + return &leftRightNotifierItem{} +} + +func (i *leftRightNotifierItem) Activate(_, _ int32) *dbus.Error { + if f := tappedLeft; f == nil { + return &dbus.ErrMsgUnknownMethod + } + + tappedLeft() + return nil +} + +func (i *leftRightNotifierItem) ContextMenu(_, _ int32) *dbus.Error { + if f := tappedRight; f == nil { + return &dbus.ErrMsgUnknownMethod + } + + tappedRight() + return nil +} + +func (i *leftRightNotifierItem) SecondaryActivate(_, _ int32) *dbus.Error { + if f := tappedRight; f == nil { + return &dbus.ErrMsgUnknownMethod + } + + tappedRight() + return nil +} + +func (i *leftRightNotifierItem) Scroll(_ int32, _ string) *dbus.Error { + return &dbus.ErrMsgUnknownMethod +} diff --git a/vendor/fyne.io/systray/systray_unix.go b/vendor/fyne.io/systray/systray_unix.go new file mode 100644 index 0000000..b7d70af --- /dev/null +++ b/vendor/fyne.io/systray/systray_unix.go @@ -0,0 +1,435 @@ +//go:build (linux || freebsd || openbsd || netbsd) && !android + +//Note that you need to have github.com/knightpp/dbus-codegen-go installed from "custom" branch +//go:generate dbus-codegen-go -prefix org.kde -package notifier -output internal/generated/notifier/status_notifier_item.go internal/StatusNotifierItem.xml +//go:generate dbus-codegen-go -prefix com.canonical -package menu -output internal/generated/menu/dbus_menu.go internal/DbusMenu.xml + +package systray + +import ( + "bytes" + "fmt" + "image" + _ "image/png" // used only here + "log" + "os" + "sync" + + "github.com/godbus/dbus/v5" + "github.com/godbus/dbus/v5/introspect" + "github.com/godbus/dbus/v5/prop" + + "fyne.io/systray/internal/generated/menu" + "fyne.io/systray/internal/generated/notifier" +) + +const ( + path = "/StatusNotifierItem" + menuPath = "/StatusNotifierMenu" +) + +var ( + // to signal quitting the internal main loop + quitChan = make(chan struct{}) + + // instance is the current instance of our DBus tray server + instance = &tray{menu: &menuLayout{}, menuVersion: 1} +) + +// SetTemplateIcon sets the systray icon as a template icon (on macOS), falling back +// to a regular icon on other platforms. +// templateIconBytes and iconBytes should be the content of .ico for windows and +// .ico/.jpg/.png for other platforms. +func SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) { + // TODO handle the templateIconBytes? + SetIcon(regularIconBytes) +} + +// SetIcon sets the systray icon. +// iconBytes should be the content of .ico for windows and .ico/.jpg/.png +// for other platforms. +func SetIcon(iconBytes []byte) { + instance.lock.Lock() + instance.iconData = iconBytes + props := instance.props + conn := instance.conn + defer instance.lock.Unlock() + + if props == nil { + return + } + + props.SetMust("org.kde.StatusNotifierItem", "IconPixmap", + []PX{convertToPixels(iconBytes)}) + if conn == nil { + return + } + + err := notifier.Emit(conn, ¬ifier.StatusNotifierItem_NewIconSignal{ + Path: path, + Body: ¬ifier.StatusNotifierItem_NewIconSignalBody{}, + }) + if err != nil { + log.Printf("systray error: failed to emit new icon signal: %s\n", err) + return + } +} + +// SetIconFromFilePath sets the systray icon from a file path. +// iconFilePath should be the path to a .ico for windows and .ico/.jpg/.png for other platforms. +func SetIconFromFilePath(iconFilePath string) error { + bytes, err := os.ReadFile(iconFilePath) + if err != nil { + return fmt.Errorf("failed to read icon file: %v", err) + } + SetIcon(bytes) + return nil +} + +// SetTitle sets the systray title, only available on Mac and Linux. +func SetTitle(t string) { + instance.lock.Lock() + instance.title = t + props := instance.props + conn := instance.conn + defer instance.lock.Unlock() + + if props == nil { + return + } + dbusErr := props.Set("org.kde.StatusNotifierItem", "Title", + dbus.MakeVariant(t)) + if dbusErr != nil { + log.Printf("systray error: failed to set Title prop: %s\n", dbusErr) + return + } + + if conn == nil { + return + } + + err := notifier.Emit(conn, ¬ifier.StatusNotifierItem_NewTitleSignal{ + Path: path, + Body: ¬ifier.StatusNotifierItem_NewTitleSignalBody{}, + }) + if err != nil { + log.Printf("systray error: failed to emit new title signal: %s\n", err) + return + } +} + +// SetTooltip sets the systray tooltip to display on mouse hover of the tray icon, +// only available on Mac and Windows. +func SetTooltip(tooltipTitle string) { + instance.lock.Lock() + instance.tooltipTitle = tooltipTitle + props := instance.props + defer instance.lock.Unlock() + + if props == nil { + return + } + dbusErr := props.Set("org.kde.StatusNotifierItem", "ToolTip", + dbus.MakeVariant(tooltip{V2: tooltipTitle})) + if dbusErr != nil { + log.Printf("systray error: failed to set ToolTip prop: %s\n", dbusErr) + return + } +} + +// SetTemplateIcon sets the icon of a menu item as a template icon (on macOS). On Windows and +// Linux, it falls back to the regular icon bytes. +// templateIconBytes and regularIconBytes should be the content of .ico for windows and +// .ico/.jpg/.png for other platforms. +func (item *MenuItem) SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) { + item.SetIcon(regularIconBytes) +} + +// SetRemovalAllowed sets whether a user can remove the systray icon or not. +// This is only supported on macOS. +func SetRemovalAllowed(allowed bool) { +} + +func setInternalLoop(_ bool) { + // nothing to action on Linux +} + +func registerSystray() { +} + +func nativeLoop() int { + nativeStart() + <-quitChan + nativeEnd() + return 0 +} + +func nativeEnd() { + runSystrayExit() + instance.conn.Close() +} + +func quit() { + close(quitChan) +} + +func nativeStart() { + systrayReady() + conn, err := dbus.SessionBus() + if err != nil { + log.Printf("systray error: failed to connect to DBus: %v\n", err) + return + } + err = notifier.ExportStatusNotifierItem(conn, path, newLeftRightNotifierItem()) + if err != nil { + log.Printf("systray error: failed to export status notifier item: %v\n", err) + } + err = menu.ExportDbusmenu(conn, menuPath, instance) + if err != nil { + log.Printf("systray error: failed to export status notifier menu: %v\n", err) + return + } + + name := fmt.Sprintf("org.kde.StatusNotifierItem-%d-1", os.Getpid()) // register id 1 for this process + _, err = conn.RequestName(name, dbus.NameFlagDoNotQueue) + if err != nil { + log.Printf("systray error: failed to request name: %s\n", err) + // it's not critical error: continue + } + props, err := prop.Export(conn, path, instance.createPropSpec()) + if err != nil { + log.Printf("systray error: failed to export notifier item properties to bus: %s\n", err) + return + } + menuProps, err := prop.Export(conn, menuPath, createMenuPropSpec()) + if err != nil { + log.Printf("systray error: failed to export notifier menu properties to bus: %s\n", err) + return + } + + node := introspect.Node{ + Name: path, + Interfaces: []introspect.Interface{ + introspect.IntrospectData, + prop.IntrospectData, + notifier.IntrospectDataStatusNotifierItem, + }, + } + err = conn.Export(introspect.NewIntrospectable(&node), path, + "org.freedesktop.DBus.Introspectable") + if err != nil { + log.Printf("systray error: failed to export node introspection: %s\n", err) + return + } + menuNode := introspect.Node{ + Name: menuPath, + Interfaces: []introspect.Interface{ + introspect.IntrospectData, + prop.IntrospectData, + menu.IntrospectDataDbusmenu, + }, + } + err = conn.Export(introspect.NewIntrospectable(&menuNode), menuPath, + "org.freedesktop.DBus.Introspectable") + if err != nil { + log.Printf("systray error: failed to export menu node introspection: %s\n", err) + return + } + + instance.lock.Lock() + instance.conn = conn + instance.props = props + instance.menuProps = menuProps + instance.lock.Unlock() + + go stayRegistered() +} + +func register() bool { + obj := instance.conn.Object("org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher") + call := obj.Call("org.kde.StatusNotifierWatcher.RegisterStatusNotifierItem", 0, path) + if call.Err != nil { + log.Printf("systray error: failed to register: %v\n", call.Err) + return false + } + + return true +} + +func stayRegistered() { + register() + + conn := instance.conn + if err := conn.AddMatchSignal( + dbus.WithMatchObjectPath("/org/freedesktop/DBus"), + dbus.WithMatchInterface("org.freedesktop.DBus"), + dbus.WithMatchSender("org.freedesktop.DBus"), + dbus.WithMatchMember("NameOwnerChanged"), + dbus.WithMatchArg(0, "org.kde.StatusNotifierWatcher"), + ); err != nil { + log.Printf("systray error: failed to register signal matching: %v\n", err) + // If we can't monitor signals, there is no point in + // us being here. we're either registered or not (per + // above) and will roll the dice from here... + return + } + + sc := make(chan *dbus.Signal, 10) + conn.Signal(sc) + + for { + select { + case sig := <-sc: + if sig == nil { + return // We get a nil signal when closing the window. + } else if len(sig.Body) < 3 { + return // malformed signal? + } + + // sig.Body has the args, which are [name old_owner new_owner] + if s, ok := sig.Body[2].(string); ok && s != "" { + register() + } + case <-quitChan: + return + } + } +} + +// tray is a basic type that handles the dbus functionality +type tray struct { + // the DBus connection that we will use + conn *dbus.Conn + + // icon data for the main systray icon + iconData []byte + // title and tooltip state + title, tooltipTitle string + + lock sync.Mutex + menu *menuLayout + menuLock sync.RWMutex + props, menuProps *prop.Properties + menuVersion uint32 +} + +func (t *tray) createPropSpec() map[string]map[string]*prop.Prop { + t.lock.Lock() + defer t.lock.Unlock() + id := t.title + if id == "" { + id = fmt.Sprintf("systray_%d", os.Getpid()) + } + return map[string]map[string]*prop.Prop{ + "org.kde.StatusNotifierItem": { + "Status": { + Value: "Active", // Passive, Active or NeedsAttention + Writable: false, + Emit: prop.EmitTrue, + Callback: nil, + }, + "Title": { + Value: t.title, + Writable: true, + Emit: prop.EmitTrue, + Callback: nil, + }, + "Id": { + Value: id, + Writable: false, + Emit: prop.EmitTrue, + Callback: nil, + }, + "Category": { + Value: "ApplicationStatus", + Writable: false, + Emit: prop.EmitTrue, + Callback: nil, + }, + "IconName": { + Value: "", + Writable: false, + Emit: prop.EmitTrue, + Callback: nil, + }, + "IconPixmap": { + Value: []PX{convertToPixels(t.iconData)}, + Writable: true, + Emit: prop.EmitTrue, + Callback: nil, + }, + "IconThemePath": { + Value: "", + Writable: false, + Emit: prop.EmitTrue, + Callback: nil, + }, + "ItemIsMenu": { + Value: tappedLeft == nil && tappedRight == nil, + Writable: false, + Emit: prop.EmitTrue, + Callback: nil, + }, + "Menu": { + Value: dbus.ObjectPath(menuPath), + Writable: true, + Emit: prop.EmitTrue, + Callback: nil, + }, + "ToolTip": { + Value: tooltip{V2: t.tooltipTitle}, + Writable: true, + Emit: prop.EmitTrue, + Callback: nil, + }, + }} +} + +// PX is picture pix map structure with width and high +type PX struct { + W, H int + Pix []byte +} + +// tooltip is our data for a tooltip property. +// Param names need to match the generated code... +type tooltip = struct { + V0 string // name + V1 []PX // icons + V2 string // title + V3 string // description +} + +func convertToPixels(data []byte) PX { + if len(data) == 0 { + return PX{} + } + + img, _, err := image.Decode(bytes.NewReader(data)) + if err != nil { + log.Printf("Failed to read icon format %v", err) + return PX{} + } + + return PX{ + img.Bounds().Dx(), img.Bounds().Dy(), + argbForImage(img), + } +} + +func argbForImage(img image.Image) []byte { + w, h := img.Bounds().Dx(), img.Bounds().Dy() + data := make([]byte, w*h*4) + i := 0 + for y := 0; y < h; y++ { + for x := 0; x < w; x++ { + r, g, b, a := img.At(x, y).RGBA() + data[i] = byte(a) + data[i+1] = byte(r) + data[i+2] = byte(g) + data[i+3] = byte(b) + i += 4 + } + } + return data +} diff --git a/vendor/fyne.io/systray/systray_windows.go b/vendor/fyne.io/systray/systray_windows.go new file mode 100644 index 0000000..84ee6a2 --- /dev/null +++ b/vendor/fyne.io/systray/systray_windows.go @@ -0,0 +1,1147 @@ +//go:build windows + +package systray + +import ( + "crypto/md5" + "encoding/hex" + "errors" + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "sort" + "sync" + "sync/atomic" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +// Helpful sources: https://github.com/golang/exp/blob/master/shiny/driver/internal/win32 + +var ( + g32 = windows.NewLazySystemDLL("Gdi32.dll") + pCreateCompatibleBitmap = g32.NewProc("CreateCompatibleBitmap") + pCreateCompatibleDC = g32.NewProc("CreateCompatibleDC") + pCreateDIBSection = g32.NewProc("CreateDIBSection") + pDeleteDC = g32.NewProc("DeleteDC") + pSelectObject = g32.NewProc("SelectObject") + + k32 = windows.NewLazySystemDLL("Kernel32.dll") + pGetModuleHandle = k32.NewProc("GetModuleHandleW") + + s32 = windows.NewLazySystemDLL("Shell32.dll") + pShellNotifyIcon = s32.NewProc("Shell_NotifyIconW") + + u32 = windows.NewLazySystemDLL("User32.dll") + pCreateMenu = u32.NewProc("CreateMenu") + pCreatePopupMenu = u32.NewProc("CreatePopupMenu") + pCreateWindowEx = u32.NewProc("CreateWindowExW") + pDefWindowProc = u32.NewProc("DefWindowProcW") + pDeleteMenu = u32.NewProc("DeleteMenu") + pDestroyMenu = u32.NewProc("DestroyMenu") + pRemoveMenu = u32.NewProc("RemoveMenu") + pDestroyWindow = u32.NewProc("DestroyWindow") + pDispatchMessage = u32.NewProc("DispatchMessageW") + pDrawIconEx = u32.NewProc("DrawIconEx") + pGetCursorPos = u32.NewProc("GetCursorPos") + pGetDC = u32.NewProc("GetDC") + pGetMessage = u32.NewProc("GetMessageW") + pGetSystemMetrics = u32.NewProc("GetSystemMetrics") + pInsertMenuItem = u32.NewProc("InsertMenuItemW") + pLoadCursor = u32.NewProc("LoadCursorW") + pLoadIcon = u32.NewProc("LoadIconW") + pLoadImage = u32.NewProc("LoadImageW") + pPostMessage = u32.NewProc("PostMessageW") + pPostQuitMessage = u32.NewProc("PostQuitMessage") + pRegisterClass = u32.NewProc("RegisterClassExW") + pRegisterWindowMessage = u32.NewProc("RegisterWindowMessageW") + pReleaseDC = u32.NewProc("ReleaseDC") + pSetForegroundWindow = u32.NewProc("SetForegroundWindow") + pSetMenuInfo = u32.NewProc("SetMenuInfo") + pSetMenuItemInfo = u32.NewProc("SetMenuItemInfoW") + pShowWindow = u32.NewProc("ShowWindow") + pTrackPopupMenu = u32.NewProc("TrackPopupMenu") + pTranslateMessage = u32.NewProc("TranslateMessage") + pUnregisterClass = u32.NewProc("UnregisterClassW") + pUpdateWindow = u32.NewProc("UpdateWindow") + + // ErrTrayNotReadyYet is returned by functions when they are called before the tray has been initialized. + ErrTrayNotReadyYet = errors.New("tray not ready yet") +) + +// Contains window class information. +// It is used with the RegisterClassEx and GetClassInfoEx functions. +// https://msdn.microsoft.com/en-us/library/ms633577.aspx +type wndClassEx struct { + Size, Style uint32 + WndProc uintptr + ClsExtra, WndExtra int32 + Instance, Icon, Cursor, Background windows.Handle + MenuName, ClassName *uint16 + IconSm windows.Handle +} + +// Registers a window class for subsequent use in calls to the CreateWindow or CreateWindowEx function. +// https://msdn.microsoft.com/en-us/library/ms633587.aspx +func (w *wndClassEx) register() error { + w.Size = uint32(unsafe.Sizeof(*w)) + res, _, err := pRegisterClass.Call(uintptr(unsafe.Pointer(w))) + if res == 0 { + return err + } + return nil +} + +// Unregisters a window class, freeing the memory required for the class. +// https://msdn.microsoft.com/en-us/library/ms644899.aspx +func (w *wndClassEx) unregister() error { + res, _, err := pUnregisterClass.Call( + uintptr(unsafe.Pointer(w.ClassName)), + uintptr(w.Instance), + ) + if res == 0 { + return err + } + return nil +} + +// Contains information that the system needs to display notifications in the notification area. +// Used by Shell_NotifyIcon. +// https://msdn.microsoft.com/en-us/library/windows/desktop/bb773352(v=vs.85).aspx +// https://msdn.microsoft.com/en-us/library/windows/desktop/bb762159 +type notifyIconData struct { + Size uint32 + Wnd windows.Handle + ID, Flags, CallbackMessage uint32 + Icon windows.Handle + Tip [128]uint16 + State, StateMask uint32 + Info [256]uint16 + Timeout, Version uint32 + InfoTitle [64]uint16 + InfoFlags uint32 + GuidItem windows.GUID + BalloonIcon windows.Handle +} + +func (nid *notifyIconData) add() error { + const NIM_ADD = 0x00000000 + res, _, err := pShellNotifyIcon.Call( + uintptr(NIM_ADD), + uintptr(unsafe.Pointer(nid)), + ) + if res == 0 { + return err + } + return nil +} + +func (nid *notifyIconData) modify() error { + const NIM_MODIFY = 0x00000001 + res, _, err := pShellNotifyIcon.Call( + uintptr(NIM_MODIFY), + uintptr(unsafe.Pointer(nid)), + ) + if res == 0 { + return err + } + return nil +} + +func (nid *notifyIconData) delete() error { + const NIM_DELETE = 0x00000002 + res, _, err := pShellNotifyIcon.Call( + uintptr(NIM_DELETE), + uintptr(unsafe.Pointer(nid)), + ) + if res == 0 { + return err + } + return nil +} + +// Contains information about a menu item. +// https://msdn.microsoft.com/en-us/library/windows/desktop/ms647578(v=vs.85).aspx +type menuItemInfo struct { + Size, Mask, Type, State uint32 + ID uint32 + SubMenu, Checked, Unchecked windows.Handle + ItemData uintptr + TypeData *uint16 + Cch uint32 + BMPItem windows.Handle +} + +// The POINT structure defines the x- and y- coordinates of a point. +// https://msdn.microsoft.com/en-us/library/windows/desktop/dd162805(v=vs.85).aspx +type point struct { + X, Y int32 +} + +// The BITMAPINFO structure defines the dimensions and color information for a DIB. +// https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfo +type bitmapInfo struct { + BmiHeader bitmapInfoHeader + BmiColors windows.Handle +} + +// The BITMAPINFOHEADER structure contains information about the dimensions and color format of a device-independent bitmap (DIB). +// https://learn.microsoft.com/en-us/previous-versions/dd183376(v=vs.85) +// https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfoheader +type bitmapInfoHeader struct { + BiSize uint32 + BiWidth int32 + BiHeight int32 + BiPlanes uint16 + BiBitCount uint16 + BiCompression uint32 + BiSizeImage uint32 + BiXPelsPerMeter int32 + BiYPelsPerMeter int32 + BiClrUsed uint32 + BiClrImportant uint32 +} + +// Contains information about loaded resources +type winTray struct { + instance, + icon, + cursor, + window windows.Handle + + loadedImages map[string]windows.Handle + muLoadedImages sync.RWMutex + // menus keeps track of the submenus keyed by the menu item ID, plus 0 + // which corresponds to the main popup menu. + menus map[uint32]windows.Handle + muMenus sync.RWMutex + // menuOf keeps track of the menu each menu item belongs to. + menuOf map[uint32]windows.Handle + muMenuOf sync.RWMutex + // menuItemIcons maintains the bitmap of each menu item (if applies). It's + // needed to show the icon correctly when showing a previously hidden menu + // item again. + menuItemIcons map[uint32]windows.Handle + muMenuItemIcons sync.RWMutex + visibleItems map[uint32][]uint32 + muVisibleItems sync.RWMutex + + nid *notifyIconData + muNID sync.RWMutex + wcex *wndClassEx + + wmSystrayMessage, + wmTaskbarCreated uint32 + + initialized atomic.Bool +} + +// isReady checks if the tray as already been initialized. It is not goroutine safe with in regard to the initialization function, but prevents a panic when functions are called too early. +func (t *winTray) isReady() bool { + return t.initialized.Load() +} + +// Loads an image from file and shows it in tray. +// Shell_NotifyIcon: https://msdn.microsoft.com/en-us/library/windows/desktop/bb762159(v=vs.85).aspx +func (t *winTray) setIcon(src string) error { + if !wt.isReady() { + return ErrTrayNotReadyYet + } + + const NIF_ICON = 0x00000002 + + h, err := t.loadIconFrom(src) + if err != nil { + return err + } + + t.muNID.Lock() + defer t.muNID.Unlock() + t.nid.Icon = h + t.nid.Flags |= NIF_ICON + t.nid.Size = uint32(unsafe.Sizeof(*t.nid)) + + return t.nid.modify() +} + +// Sets tooltip on icon. +// Shell_NotifyIcon: https://msdn.microsoft.com/en-us/library/windows/desktop/bb762159(v=vs.85).aspx +func (t *winTray) setTooltip(src string) error { + if !wt.isReady() { + return ErrTrayNotReadyYet + } + + const NIF_TIP = 0x00000004 + b, err := windows.UTF16FromString(src) + if err != nil { + return err + } + + t.muNID.Lock() + defer t.muNID.Unlock() + copy(t.nid.Tip[:], b[:]) + t.nid.Flags |= NIF_TIP + t.nid.Size = uint32(unsafe.Sizeof(*t.nid)) + + return t.nid.modify() +} + +var wt = winTray{} + +// WindowProc callback function that processes messages sent to a window. +// https://msdn.microsoft.com/en-us/library/windows/desktop/ms633573(v=vs.85).aspx +func (t *winTray) wndProc(hWnd windows.Handle, message uint32, wParam, lParam uintptr) (lResult uintptr) { + const ( + WM_RBUTTONUP = 0x0205 + WM_LBUTTONUP = 0x0202 + WM_COMMAND = 0x0111 + WM_ENDSESSION = 0x0016 + WM_CLOSE = 0x0010 + WM_DESTROY = 0x0002 + ) + switch message { + case WM_COMMAND: + menuItemId := int32(wParam) + // https://docs.microsoft.com/en-us/windows/win32/menurc/wm-command#menus + if menuItemId != -1 { + systrayMenuItemSelected(uint32(wParam)) + } + case WM_CLOSE: + pDestroyWindow.Call(uintptr(t.window)) + t.wcex.unregister() + case WM_DESTROY: + // same as WM_ENDSESSION, but throws 0 exit code after all + defer pPostQuitMessage.Call(uintptr(int32(0))) + fallthrough + case WM_ENDSESSION: + t.muNID.Lock() + if t.nid != nil { + t.nid.delete() + } + t.muNID.Unlock() + runSystrayExit() + case t.wmSystrayMessage: + switch lParam { + case WM_LBUTTONUP: + systrayLeftClick() + case WM_RBUTTONUP: + systrayRightClick() + } + case t.wmTaskbarCreated: // on explorer.exe restarts + t.muNID.Lock() + t.nid.add() + t.muNID.Unlock() + default: + // Calls the default window procedure to provide default processing for any window messages that an application does not process. + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms633572(v=vs.85).aspx + lResult, _, _ = pDefWindowProc.Call( + uintptr(hWnd), + uintptr(message), + uintptr(wParam), + uintptr(lParam), + ) + } + return +} + +func (t *winTray) initInstance() error { + const IDI_APPLICATION = 32512 + const IDC_ARROW = 32512 // Standard arrow + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms633548(v=vs.85).aspx + const SW_HIDE = 0 + const CW_USEDEFAULT = 0x80000000 + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms632600(v=vs.85).aspx + const ( + WS_CAPTION = 0x00C00000 + WS_MAXIMIZEBOX = 0x00010000 + WS_MINIMIZEBOX = 0x00020000 + WS_OVERLAPPED = 0x00000000 + WS_SYSMENU = 0x00080000 + WS_THICKFRAME = 0x00040000 + + WS_OVERLAPPEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX + ) + // https://msdn.microsoft.com/en-us/library/windows/desktop/ff729176 + const ( + CS_HREDRAW = 0x0002 + CS_VREDRAW = 0x0001 + ) + const NIF_MESSAGE = 0x00000001 + + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms644931(v=vs.85).aspx + const WM_USER = 0x0400 + + const ( + className = "SystrayClass" + windowName = "" + ) + + t.wmSystrayMessage = WM_USER + 1 + t.visibleItems = make(map[uint32][]uint32) + t.menus = make(map[uint32]windows.Handle) + t.menuOf = make(map[uint32]windows.Handle) + t.menuItemIcons = make(map[uint32]windows.Handle) + + taskbarEventNamePtr, _ := windows.UTF16PtrFromString("TaskbarCreated") + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms644947 + res, _, err := pRegisterWindowMessage.Call( + uintptr(unsafe.Pointer(taskbarEventNamePtr)), + ) + t.wmTaskbarCreated = uint32(res) + + t.loadedImages = make(map[string]windows.Handle) + + instanceHandle, _, err := pGetModuleHandle.Call(0) + if instanceHandle == 0 { + return err + } + t.instance = windows.Handle(instanceHandle) + + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms648072(v=vs.85).aspx + iconHandle, _, err := pLoadIcon.Call(0, uintptr(IDI_APPLICATION)) + if iconHandle == 0 { + return err + } + t.icon = windows.Handle(iconHandle) + + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms648391(v=vs.85).aspx + cursorHandle, _, err := pLoadCursor.Call(0, uintptr(IDC_ARROW)) + if cursorHandle == 0 { + return err + } + t.cursor = windows.Handle(cursorHandle) + + classNamePtr, err := windows.UTF16PtrFromString(className) + if err != nil { + return err + } + + windowNamePtr, err := windows.UTF16PtrFromString(windowName) + if err != nil { + return err + } + + t.wcex = &wndClassEx{ + Style: CS_HREDRAW | CS_VREDRAW, + WndProc: windows.NewCallback(t.wndProc), + Instance: t.instance, + Icon: t.icon, + Cursor: t.cursor, + Background: windows.Handle(6), // (COLOR_WINDOW + 1) + ClassName: classNamePtr, + IconSm: t.icon, + } + if err := t.wcex.register(); err != nil { + return err + } + + windowHandle, _, err := pCreateWindowEx.Call( + uintptr(0), + uintptr(unsafe.Pointer(classNamePtr)), + uintptr(unsafe.Pointer(windowNamePtr)), + uintptr(WS_OVERLAPPEDWINDOW), + uintptr(CW_USEDEFAULT), + uintptr(CW_USEDEFAULT), + uintptr(CW_USEDEFAULT), + uintptr(CW_USEDEFAULT), + uintptr(0), + uintptr(0), + uintptr(t.instance), + uintptr(0), + ) + if windowHandle == 0 { + return err + } + t.window = windows.Handle(windowHandle) + + pShowWindow.Call( + uintptr(t.window), + uintptr(SW_HIDE), + ) + + pUpdateWindow.Call( + uintptr(t.window), + ) + + t.muNID.Lock() + defer t.muNID.Unlock() + t.nid = ¬ifyIconData{ + Wnd: windows.Handle(t.window), + ID: 100, + Flags: NIF_MESSAGE, + CallbackMessage: t.wmSystrayMessage, + } + t.nid.Size = uint32(unsafe.Sizeof(*t.nid)) + + return t.nid.add() +} + +func (t *winTray) createMenu() error { + const MIM_APPLYTOSUBMENUS = 0x80000000 // Settings apply to the menu and all of its submenus + + menuHandle, _, err := pCreatePopupMenu.Call() + if menuHandle == 0 { + return err + } + t.menus[0] = windows.Handle(menuHandle) + + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms647575(v=vs.85).aspx + mi := struct { + Size, Mask, Style, Max uint32 + Background windows.Handle + ContextHelpID uint32 + MenuData uintptr + }{ + Mask: MIM_APPLYTOSUBMENUS, + } + mi.Size = uint32(unsafe.Sizeof(mi)) + + res, _, err := pSetMenuInfo.Call( + uintptr(t.menus[0]), + uintptr(unsafe.Pointer(&mi)), + ) + if res == 0 { + return err + } + return nil +} + +func (t *winTray) convertToSubMenu(menuItemId uint32) (windows.Handle, error) { + const MIIM_SUBMENU = 0x00000004 + + res, _, err := pCreateMenu.Call() + if res == 0 { + return 0, err + } + menu := windows.Handle(res) + + mi := menuItemInfo{Mask: MIIM_SUBMENU, SubMenu: menu} + mi.Size = uint32(unsafe.Sizeof(mi)) + t.muMenuOf.RLock() + hMenu := t.menuOf[menuItemId] + t.muMenuOf.RUnlock() + res, _, err = pSetMenuItemInfo.Call( + uintptr(hMenu), + uintptr(menuItemId), + 0, + uintptr(unsafe.Pointer(&mi)), + ) + if res == 0 { + return 0, err + } + t.muMenus.Lock() + t.menus[menuItemId] = menu + t.muMenus.Unlock() + return menu, nil +} + +// SetRemovalAllowed sets whether a user can remove the systray icon or not. +// This is only supported on macOS. +func SetRemovalAllowed(allowed bool) { +} + +func (t *winTray) addOrUpdateMenuItem(menuItemId uint32, parentId uint32, title string, disabled, checked bool) error { + if !wt.isReady() { + return ErrTrayNotReadyYet + } + + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms647578(v=vs.85).aspx + const ( + MIIM_FTYPE = 0x00000100 + MIIM_BITMAP = 0x00000080 + MIIM_STRING = 0x00000040 + MIIM_SUBMENU = 0x00000004 + MIIM_ID = 0x00000002 + MIIM_STATE = 0x00000001 + ) + const MFT_STRING = 0x00000000 + const ( + MFS_CHECKED = 0x00000008 + MFS_DISABLED = 0x00000003 + ) + titlePtr, err := windows.UTF16PtrFromString(title) + if err != nil { + return err + } + + mi := menuItemInfo{ + Mask: MIIM_FTYPE | MIIM_STRING | MIIM_ID | MIIM_STATE, + Type: MFT_STRING, + ID: uint32(menuItemId), + TypeData: titlePtr, + Cch: uint32(len(title)), + } + mi.Size = uint32(unsafe.Sizeof(mi)) + if disabled { + mi.State |= MFS_DISABLED + } + if checked { + mi.State |= MFS_CHECKED + } + t.muMenuItemIcons.RLock() + hIcon := t.menuItemIcons[menuItemId] + t.muMenuItemIcons.RUnlock() + if hIcon > 0 { + mi.Mask |= MIIM_BITMAP + mi.BMPItem = hIcon + } + + var res uintptr + t.muMenus.RLock() + menu, exists := t.menus[parentId] + t.muMenus.RUnlock() + if !exists { + menu, err = t.convertToSubMenu(parentId) + if err != nil { + return err + } + t.muMenus.Lock() + t.menus[parentId] = menu + t.muMenus.Unlock() + } else if t.getVisibleItemIndex(parentId, menuItemId) != -1 { + // We set the menu item info based on the menuID + res, _, err = pSetMenuItemInfo.Call( + uintptr(menu), + uintptr(menuItemId), + 0, + uintptr(unsafe.Pointer(&mi)), + ) + } + + if res == 0 { + // Menu item does not already exist, create it + t.muMenus.RLock() + submenu, exists := t.menus[menuItemId] + t.muMenus.RUnlock() + if exists { + mi.Mask |= MIIM_SUBMENU + mi.SubMenu = submenu + } + t.addToVisibleItems(parentId, menuItemId) + position := t.getVisibleItemIndex(parentId, menuItemId) + res, _, err = pInsertMenuItem.Call( + uintptr(menu), + uintptr(position), + 1, + uintptr(unsafe.Pointer(&mi)), + ) + if res == 0 { + t.delFromVisibleItems(parentId, menuItemId) + return err + } + t.muMenuOf.Lock() + t.menuOf[menuItemId] = menu + t.muMenuOf.Unlock() + } + + return nil +} + +func (t *winTray) addSeparatorMenuItem(menuItemId, parentId uint32) error { + if !wt.isReady() { + return ErrTrayNotReadyYet + } + + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms647578(v=vs.85).aspx + const ( + MIIM_FTYPE = 0x00000100 + MIIM_ID = 0x00000002 + MIIM_STATE = 0x00000001 + ) + const MFT_SEPARATOR = 0x00000800 + + mi := menuItemInfo{ + Mask: MIIM_FTYPE | MIIM_ID | MIIM_STATE, + Type: MFT_SEPARATOR, + ID: uint32(menuItemId), + } + + mi.Size = uint32(unsafe.Sizeof(mi)) + + t.addToVisibleItems(parentId, menuItemId) + position := t.getVisibleItemIndex(parentId, menuItemId) + t.muMenus.RLock() + menu := uintptr(t.menus[parentId]) + t.muMenus.RUnlock() + res, _, err := pInsertMenuItem.Call( + menu, + uintptr(position), + 1, + uintptr(unsafe.Pointer(&mi)), + ) + if res == 0 { + return err + } + + return nil +} + +func (t *winTray) removeMenuItem(menuItemId, parentId uint32) error { + if !wt.isReady() { + return ErrTrayNotReadyYet + } + + const MF_BYCOMMAND = 0x00000000 + const ERROR_SUCCESS syscall.Errno = 0 + + t.muMenus.RLock() + menu := uintptr(t.menus[parentId]) + t.muMenus.RUnlock() + res, _, err := pDeleteMenu.Call( + menu, + uintptr(menuItemId), + MF_BYCOMMAND, + ) + if res == 0 && err.(syscall.Errno) != ERROR_SUCCESS { + return err + } + t.delFromVisibleItems(parentId, menuItemId) + + return nil +} + +func (t *winTray) hideMenuItem(menuItemId, parentId uint32) error { + if !wt.isReady() { + return ErrTrayNotReadyYet + } + + const MF_BYCOMMAND = 0x00000000 + const ERROR_SUCCESS syscall.Errno = 0 + + t.muMenus.RLock() + menu := uintptr(t.menus[parentId]) + t.muMenus.RUnlock() + res, _, err := pRemoveMenu.Call( + menu, + uintptr(menuItemId), + MF_BYCOMMAND, + ) + if res == 0 && err.(syscall.Errno) != ERROR_SUCCESS { + return err + } + t.delFromVisibleItems(parentId, menuItemId) + + return nil +} + +func (t *winTray) showMenu() error { + if !wt.isReady() { + return ErrTrayNotReadyYet + } + + const ( + TPM_BOTTOMALIGN = 0x0020 + TPM_LEFTALIGN = 0x0000 + ) + p := point{} + res, _, err := pGetCursorPos.Call(uintptr(unsafe.Pointer(&p))) + if res == 0 { + return err + } + pSetForegroundWindow.Call(uintptr(t.window)) + + res, _, err = pTrackPopupMenu.Call( + uintptr(t.menus[0]), + TPM_BOTTOMALIGN|TPM_LEFTALIGN, + uintptr(p.X), + uintptr(p.Y), + 0, + uintptr(t.window), + 0, + ) + if res == 0 { + return err + } + + return nil +} + +func (t *winTray) delFromVisibleItems(parent, val uint32) { + t.muVisibleItems.Lock() + defer t.muVisibleItems.Unlock() + visibleItems := t.visibleItems[parent] + for i, itemval := range visibleItems { + if val == itemval { + t.visibleItems[parent] = append(visibleItems[:i], visibleItems[i+1:]...) + break + } + } +} + +func (t *winTray) addToVisibleItems(parent, val uint32) { + t.muVisibleItems.Lock() + defer t.muVisibleItems.Unlock() + if visibleItems, exists := t.visibleItems[parent]; !exists { + t.visibleItems[parent] = []uint32{val} + } else { + newvisible := append(visibleItems, val) + sort.Slice(newvisible, func(i, j int) bool { return newvisible[i] < newvisible[j] }) + t.visibleItems[parent] = newvisible + } +} + +func (t *winTray) getVisibleItemIndex(parent, val uint32) int { + t.muVisibleItems.RLock() + defer t.muVisibleItems.RUnlock() + for i, itemval := range t.visibleItems[parent] { + if val == itemval { + return i + } + } + return -1 +} + +// Loads an image from file to be shown in tray or menu item. +// LoadImage: https://msdn.microsoft.com/en-us/library/windows/desktop/ms648045(v=vs.85).aspx +func (t *winTray) loadIconFrom(src string) (windows.Handle, error) { + if !wt.isReady() { + return 0, ErrTrayNotReadyYet + } + + const IMAGE_ICON = 1 // Loads an icon + const LR_LOADFROMFILE = 0x00000010 // Loads the stand-alone image from the file + const LR_DEFAULTSIZE = 0x00000040 // Loads default-size icon for windows(SM_CXICON x SM_CYICON) if cx, cy are set to zero + + // Save and reuse handles of loaded images + t.muLoadedImages.RLock() + h, ok := t.loadedImages[src] + t.muLoadedImages.RUnlock() + if !ok { + srcPtr, err := windows.UTF16PtrFromString(src) + if err != nil { + return 0, err + } + res, _, err := pLoadImage.Call( + 0, + uintptr(unsafe.Pointer(srcPtr)), + IMAGE_ICON, + 0, + 0, + LR_LOADFROMFILE|LR_DEFAULTSIZE, + ) + if res == 0 { + return 0, err + } + h = windows.Handle(res) + t.muLoadedImages.Lock() + t.loadedImages[src] = h + t.muLoadedImages.Unlock() + } + return h, nil +} + +func iconToBitmap(hIcon windows.Handle) (windows.Handle, error) { + const SM_CXSMICON = 49 + const SM_CYSMICON = 50 + const DI_NORMAL = 0x3 + hDC, _, err := pGetDC.Call(uintptr(0)) + if hDC == 0 { + return 0, err + } + defer pReleaseDC.Call(uintptr(0), hDC) + hMemDC, _, err := pCreateCompatibleDC.Call(hDC) + if hMemDC == 0 { + return 0, err + } + defer pDeleteDC.Call(hMemDC) + cx, _, _ := pGetSystemMetrics.Call(SM_CXSMICON) + cy, _, _ := pGetSystemMetrics.Call(SM_CYSMICON) + hMemBmp, err := create32BitHBitmap(hMemDC, int32(cx), int32(cy)) + hOriginalBmp, _, _ := pSelectObject.Call(hMemDC, hMemBmp) + defer pSelectObject.Call(hMemDC, hOriginalBmp) + res, _, err := pDrawIconEx.Call(hMemDC, 0, 0, uintptr(hIcon), cx, cy, 0, uintptr(0), DI_NORMAL) + if res == 0 { + return 0, err + } + return windows.Handle(hMemBmp), nil +} + +// https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-createdibsection +func create32BitHBitmap(hDC uintptr, cx, cy int32) (uintptr, error) { + const BI_RGB uint32 = 0 + const DIB_RGB_COLORS = 0 + bmi := bitmapInfo{ + BmiHeader: bitmapInfoHeader{ + BiPlanes: 1, + BiCompression: BI_RGB, + BiWidth: cx, + BiHeight: cy, + BiBitCount: 32, + }, + } + bmi.BmiHeader.BiSize = uint32(unsafe.Sizeof(bmi.BmiHeader)) + var bits uintptr + hBitmap, _, err := pCreateDIBSection.Call( + hDC, + uintptr(unsafe.Pointer(&bmi)), + DIB_RGB_COLORS, + uintptr(unsafe.Pointer(&bits)), + uintptr(0), + 0, + ) + if hBitmap == 0 { + return 0, err + } + return hBitmap, nil +} + +func registerSystray() { + if err := wt.initInstance(); err != nil { + log.Printf("systray error: unable to init instance: %s\n", err) + return + } + + if err := wt.createMenu(); err != nil { + log.Printf("systray error: unable to create menu: %s\n", err) + return + } + + wt.initialized.Store(true) + systrayReady() +} + +var m = &struct { + WindowHandle windows.Handle + Message uint32 + Wparam uintptr + Lparam uintptr + Time uint32 + Pt point +}{} + +func nativeLoop() { + for doNativeTick() { + } +} + +func nativeEnd() { +} + +func nativeStart() { + go func() { + for doNativeTick() { + } + }() +} + +func doNativeTick() bool { + ret, _, err := pGetMessage.Call(uintptr(unsafe.Pointer(m)), 0, 0, 0) + + // If the function retrieves a message other than WM_QUIT, the return value is nonzero. + // If the function retrieves the WM_QUIT message, the return value is zero. + // If there is an error, the return value is -1 + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms644936(v=vs.85).aspx + switch int32(ret) { + case -1: + log.Printf("systray error: message loop failure: %s\n", err) + return false + case 0: + return false + default: + pTranslateMessage.Call(uintptr(unsafe.Pointer(m))) + pDispatchMessage.Call(uintptr(unsafe.Pointer(m))) + } + return true +} + +func quit() { + const WM_CLOSE = 0x0010 + + pPostMessage.Call( + uintptr(wt.window), + WM_CLOSE, + 0, + 0, + ) + + wt.muNID.Lock() + if wt.nid != nil { + wt.nid.delete() + } + wt.muNID.Unlock() + runSystrayExit() +} + +func setInternalLoop(bool) { +} + +func iconBytesToFilePath(iconBytes []byte) (string, error) { + bh := md5.Sum(iconBytes) + dataHash := hex.EncodeToString(bh[:]) + iconFilePath := filepath.Join(os.TempDir(), "systray_temp_icon_"+dataHash) + + if _, err := os.Stat(iconFilePath); os.IsNotExist(err) { + if err := ioutil.WriteFile(iconFilePath, iconBytes, 0644); err != nil { + return "", err + } + } + return iconFilePath, nil +} + +// SetIcon sets the systray icon. +// iconBytes should be the content of .ico for windows and .ico/.jpg/.png +// for other platforms. +func SetIcon(iconBytes []byte) { + iconFilePath, err := iconBytesToFilePath(iconBytes) + if err != nil { + log.Printf("systray error: unable to write icon data to temp file: %s\n", err) + return + } + if err := wt.setIcon(iconFilePath); err != nil { + log.Printf("systray error: unable to set icon: %s\n", err) + return + } +} + +// SetIconFromFilePath sets the systray icon from a file path. +// iconFilePath should be the path to a .ico for windows and .ico/.jpg/.png for other platforms. +func SetIconFromFilePath(iconFilePath string) error { + err := wt.setIcon(iconFilePath) + if err != nil { + return fmt.Errorf("failed to set icon: %v", err) + } + return nil +} + +// SetTemplateIcon sets the systray icon as a template icon (on macOS), falling back +// to a regular icon on other platforms. +// templateIconBytes and iconBytes should be the content of .ico for windows and +// .ico/.jpg/.png for other platforms. +func SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) { + SetIcon(regularIconBytes) +} + +// SetTitle sets the systray title, only available on Mac and Linux. +func SetTitle(title string) { + // do nothing +} + +func (item *MenuItem) parentId() uint32 { + if item.parent != nil { + return uint32(item.parent.id) + } + return 0 +} + +// SetIcon sets the icon of a menu item. Only works on macOS and Windows. +// iconBytes should be the content of .ico/.jpg/.png +func (item *MenuItem) SetIcon(iconBytes []byte) { + iconFilePath, err := iconBytesToFilePath(iconBytes) + if err != nil { + log.Printf("systray error: unable to write icon data to temp file: %s\n", err) + return + } + + err = item.SetIconFromFilePath(iconFilePath) + if err != nil { + log.Printf("systray error: %s\n", err) + return + } +} + +// SetIconFromFilePath sets the icon of a menu item from a file path. +// iconFilePath should be the path to a .ico for windows and .ico/.jpg/.png for other platforms. +func (item *MenuItem) SetIconFromFilePath(iconFilePath string) error { + h, err := wt.loadIconFrom(iconFilePath) + if err != nil { + return fmt.Errorf("unable to load icon from file: %s", err) + } + + h, err = iconToBitmap(h) + if err != nil { + return fmt.Errorf("unable to convert icon to bitmap: %s", err) + } + wt.muMenuItemIcons.Lock() + wt.menuItemIcons[uint32(item.id)] = h + wt.muMenuItemIcons.Unlock() + + err = wt.addOrUpdateMenuItem(uint32(item.id), item.parentId(), item.title, item.disabled, item.checked) + if err != nil { + return fmt.Errorf("unable to addOrUpdateMenuItem: %s", err) + } + return nil +} + +// SetTooltip sets the systray tooltip to display on mouse hover of the tray icon, +// only available on Mac and Windows. +func SetTooltip(tooltip string) { + if err := wt.setTooltip(tooltip); err != nil { + log.Printf("systray error: unable to set tooltip: %s\n", err) + return + } +} + +func addOrUpdateMenuItem(item *MenuItem) { + err := wt.addOrUpdateMenuItem(uint32(item.id), item.parentId(), item.title, item.disabled, item.checked) + if err != nil { + log.Printf("systray error: unable to addOrUpdateMenuItem: %s\n", err) + return + } +} + +// SetTemplateIcon sets the icon of a menu item as a template icon (on macOS). On Windows, it +// falls back to the regular icon bytes and on Linux it does nothing. +// templateIconBytes and regularIconBytes should be the content of .ico for windows and +// .ico/.jpg/.png for other platforms. +func (item *MenuItem) SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) { + item.SetIcon(regularIconBytes) +} + +func addSeparator(id uint32, parent uint32) { + err := wt.addSeparatorMenuItem(id, parent) + if err != nil { + log.Printf("systray error: unable to addSeparator: %s\n", err) + return + } +} + +func hideMenuItem(item *MenuItem) { + err := wt.hideMenuItem(uint32(item.id), item.parentId()) + if err != nil { + log.Printf("systray error: unable to hideMenuItem: %s\n", err) + return + } +} + +func removeMenuItem(item *MenuItem) { + err := wt.removeMenuItem(uint32(item.id), item.parentId()) + if err != nil { + log.Printf("systray error: unable to removeMenuItem: %s\n", err) + return + } +} + +func showMenuItem(item *MenuItem) { + addOrUpdateMenuItem(item) +} + +func resetMenu() { + _, _, _ = pDestroyMenu.Call(uintptr(wt.menus[0])) + wt.visibleItems = make(map[uint32][]uint32) + wt.menus = make(map[uint32]windows.Handle) + wt.menuOf = make(map[uint32]windows.Handle) + wt.menuItemIcons = make(map[uint32]windows.Handle) + wt.createMenu() +} + +func systrayLeftClick() { + if fn := tappedLeft; fn != nil { + fn() + return + } + + wt.showMenu() +} + +func systrayRightClick() { + if fn := tappedRight; fn != nil { + fn() + return + } + + wt.showMenu() +} diff --git a/vendor/github.com/BurntSushi/toml/.gitignore b/vendor/github.com/BurntSushi/toml/.gitignore new file mode 100644 index 0000000..fe79e3a --- /dev/null +++ b/vendor/github.com/BurntSushi/toml/.gitignore @@ -0,0 +1,2 @@ +/toml.test +/toml-test diff --git a/vendor/github.com/BurntSushi/toml/COPYING b/vendor/github.com/BurntSushi/toml/COPYING new file mode 100644 index 0000000..01b5743 --- /dev/null +++ b/vendor/github.com/BurntSushi/toml/COPYING @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013 TOML authors + +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: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +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. diff --git a/vendor/github.com/BurntSushi/toml/README.md b/vendor/github.com/BurntSushi/toml/README.md new file mode 100644 index 0000000..235496e --- /dev/null +++ b/vendor/github.com/BurntSushi/toml/README.md @@ -0,0 +1,120 @@ +TOML stands for Tom's Obvious, Minimal Language. This Go package provides a +reflection interface similar to Go's standard library `json` and `xml` packages. + +Compatible with TOML version [v1.0.0](https://toml.io/en/v1.0.0). + +Documentation: https://pkg.go.dev/github.com/BurntSushi/toml + +See the [releases page](https://github.com/BurntSushi/toml/releases) for a +changelog; this information is also in the git tag annotations (e.g. `git show +v0.4.0`). + +This library requires Go 1.18 or newer; add it to your go.mod with: + + % go get github.com/BurntSushi/toml@latest + +It also comes with a TOML validator CLI tool: + + % go install github.com/BurntSushi/toml/cmd/tomlv@latest + % tomlv some-toml-file.toml + +### Examples +For the simplest example, consider some TOML file as just a list of keys and +values: + +```toml +Age = 25 +Cats = [ "Cauchy", "Plato" ] +Pi = 3.14 +Perfection = [ 6, 28, 496, 8128 ] +DOB = 1987-07-05T05:45:00Z +``` + +Which can be decoded with: + +```go +type Config struct { + Age int + Cats []string + Pi float64 + Perfection []int + DOB time.Time +} + +var conf Config +_, err := toml.Decode(tomlData, &conf) +``` + +You can also use struct tags if your struct field name doesn't map to a TOML key +value directly: + +```toml +some_key_NAME = "wat" +``` + +```go +type TOML struct { + ObscureKey string `toml:"some_key_NAME"` +} +``` + +Beware that like other decoders **only exported fields** are considered when +encoding and decoding; private fields are silently ignored. + +### Using the `Marshaler` and `encoding.TextUnmarshaler` interfaces +Here's an example that automatically parses values in a `mail.Address`: + +```toml +contacts = [ + "Donald Duck ", + "Scrooge McDuck ", +] +``` + +Can be decoded with: + +```go +// Create address type which satisfies the encoding.TextUnmarshaler interface. +type address struct { + *mail.Address +} + +func (a *address) UnmarshalText(text []byte) error { + var err error + a.Address, err = mail.ParseAddress(string(text)) + return err +} + +// Decode it. +func decode() { + blob := ` + contacts = [ + "Donald Duck ", + "Scrooge McDuck ", + ] + ` + + var contacts struct { + Contacts []address + } + + _, err := toml.Decode(blob, &contacts) + if err != nil { + log.Fatal(err) + } + + for _, c := range contacts.Contacts { + fmt.Printf("%#v\n", c.Address) + } + + // Output: + // &mail.Address{Name:"Donald Duck", Address:"donald@duckburg.com"} + // &mail.Address{Name:"Scrooge McDuck", Address:"scrooge@duckburg.com"} +} +``` + +To target TOML specifically you can implement `UnmarshalTOML` TOML interface in +a similar way. + +### More complex usage +See the [`_example/`](/_example) directory for a more complex example. diff --git a/vendor/github.com/BurntSushi/toml/decode.go b/vendor/github.com/BurntSushi/toml/decode.go new file mode 100644 index 0000000..3fa516c --- /dev/null +++ b/vendor/github.com/BurntSushi/toml/decode.go @@ -0,0 +1,638 @@ +package toml + +import ( + "bytes" + "encoding" + "encoding/json" + "fmt" + "io" + "io/fs" + "math" + "os" + "reflect" + "strconv" + "strings" + "time" +) + +// Unmarshaler is the interface implemented by objects that can unmarshal a +// TOML description of themselves. +type Unmarshaler interface { + UnmarshalTOML(any) error +} + +// Unmarshal decodes the contents of data in TOML format into a pointer v. +// +// See [Decoder] for a description of the decoding process. +func Unmarshal(data []byte, v any) error { + _, err := NewDecoder(bytes.NewReader(data)).Decode(v) + return err +} + +// Decode the TOML data in to the pointer v. +// +// See [Decoder] for a description of the decoding process. +func Decode(data string, v any) (MetaData, error) { + return NewDecoder(strings.NewReader(data)).Decode(v) +} + +// DecodeFile reads the contents of a file and decodes it with [Decode]. +func DecodeFile(path string, v any) (MetaData, error) { + fp, err := os.Open(path) + if err != nil { + return MetaData{}, err + } + defer fp.Close() + return NewDecoder(fp).Decode(v) +} + +// DecodeFS reads the contents of a file from [fs.FS] and decodes it with +// [Decode]. +func DecodeFS(fsys fs.FS, path string, v any) (MetaData, error) { + fp, err := fsys.Open(path) + if err != nil { + return MetaData{}, err + } + defer fp.Close() + return NewDecoder(fp).Decode(v) +} + +// Primitive is a TOML value that hasn't been decoded into a Go value. +// +// This type can be used for any value, which will cause decoding to be delayed. +// You can use [PrimitiveDecode] to "manually" decode these values. +// +// NOTE: The underlying representation of a `Primitive` value is subject to +// change. Do not rely on it. +// +// NOTE: Primitive values are still parsed, so using them will only avoid the +// overhead of reflection. They can be useful when you don't know the exact type +// of TOML data until runtime. +type Primitive struct { + undecoded any + context Key +} + +// The significand precision for float32 and float64 is 24 and 53 bits; this is +// the range a natural number can be stored in a float without loss of data. +const ( + maxSafeFloat32Int = 16777215 // 2^24-1 + maxSafeFloat64Int = int64(9007199254740991) // 2^53-1 +) + +// Decoder decodes TOML data. +// +// TOML tables correspond to Go structs or maps; they can be used +// interchangeably, but structs offer better type safety. +// +// TOML table arrays correspond to either a slice of structs or a slice of maps. +// +// TOML datetimes correspond to [time.Time]. Local datetimes are parsed in the +// local timezone. +// +// [time.Duration] types are treated as nanoseconds if the TOML value is an +// integer, or they're parsed with time.ParseDuration() if they're strings. +// +// All other TOML types (float, string, int, bool and array) correspond to the +// obvious Go types. +// +// An exception to the above rules is if a type implements the TextUnmarshaler +// interface, in which case any primitive TOML value (floats, strings, integers, +// booleans, datetimes) will be converted to a []byte and given to the value's +// UnmarshalText method. See the Unmarshaler example for a demonstration with +// email addresses. +// +// # Key mapping +// +// TOML keys can map to either keys in a Go map or field names in a Go struct. +// The special `toml` struct tag can be used to map TOML keys to struct fields +// that don't match the key name exactly (see the example). A case insensitive +// match to struct names will be tried if an exact match can't be found. +// +// The mapping between TOML values and Go values is loose. That is, there may +// exist TOML values that cannot be placed into your representation, and there +// may be parts of your representation that do not correspond to TOML values. +// This loose mapping can be made stricter by using the IsDefined and/or +// Undecoded methods on the MetaData returned. +// +// This decoder does not handle cyclic types. Decode will not terminate if a +// cyclic type is passed. +type Decoder struct { + r io.Reader +} + +// NewDecoder creates a new Decoder. +func NewDecoder(r io.Reader) *Decoder { + return &Decoder{r: r} +} + +var ( + unmarshalToml = reflect.TypeOf((*Unmarshaler)(nil)).Elem() + unmarshalText = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem() + primitiveType = reflect.TypeOf((*Primitive)(nil)).Elem() +) + +// Decode TOML data in to the pointer `v`. +func (dec *Decoder) Decode(v any) (MetaData, error) { + rv := reflect.ValueOf(v) + if rv.Kind() != reflect.Ptr { + s := "%q" + if reflect.TypeOf(v) == nil { + s = "%v" + } + + return MetaData{}, fmt.Errorf("toml: cannot decode to non-pointer "+s, reflect.TypeOf(v)) + } + if rv.IsNil() { + return MetaData{}, fmt.Errorf("toml: cannot decode to nil value of %q", reflect.TypeOf(v)) + } + + // Check if this is a supported type: struct, map, any, or something that + // implements UnmarshalTOML or UnmarshalText. + rv = indirect(rv) + rt := rv.Type() + if rv.Kind() != reflect.Struct && rv.Kind() != reflect.Map && + !(rv.Kind() == reflect.Interface && rv.NumMethod() == 0) && + !rt.Implements(unmarshalToml) && !rt.Implements(unmarshalText) { + return MetaData{}, fmt.Errorf("toml: cannot decode to type %s", rt) + } + + // TODO: parser should read from io.Reader? Or at the very least, make it + // read from []byte rather than string + data, err := io.ReadAll(dec.r) + if err != nil { + return MetaData{}, err + } + + p, err := parse(string(data)) + if err != nil { + return MetaData{}, err + } + + md := MetaData{ + mapping: p.mapping, + keyInfo: p.keyInfo, + keys: p.ordered, + decoded: make(map[string]struct{}, len(p.ordered)), + context: nil, + data: data, + } + return md, md.unify(p.mapping, rv) +} + +// PrimitiveDecode is just like the other Decode* functions, except it decodes a +// TOML value that has already been parsed. Valid primitive values can *only* be +// obtained from values filled by the decoder functions, including this method. +// (i.e., v may contain more [Primitive] values.) +// +// Meta data for primitive values is included in the meta data returned by the +// Decode* functions with one exception: keys returned by the Undecoded method +// will only reflect keys that were decoded. Namely, any keys hidden behind a +// Primitive will be considered undecoded. Executing this method will update the +// undecoded keys in the meta data. (See the example.) +func (md *MetaData) PrimitiveDecode(primValue Primitive, v any) error { + md.context = primValue.context + defer func() { md.context = nil }() + return md.unify(primValue.undecoded, rvalue(v)) +} + +// markDecodedRecursive is a helper to mark any key under the given tmap as +// decoded, recursing as needed +func markDecodedRecursive(md *MetaData, tmap map[string]any) { + for key := range tmap { + md.decoded[md.context.add(key).String()] = struct{}{} + if tmap, ok := tmap[key].(map[string]any); ok { + md.context = append(md.context, key) + markDecodedRecursive(md, tmap) + md.context = md.context[0 : len(md.context)-1] + } + } +} + +// unify performs a sort of type unification based on the structure of `rv`, +// which is the client representation. +// +// Any type mismatch produces an error. Finding a type that we don't know +// how to handle produces an unsupported type error. +func (md *MetaData) unify(data any, rv reflect.Value) error { + // Special case. Look for a `Primitive` value. + // TODO: #76 would make this superfluous after implemented. + if rv.Type() == primitiveType { + // Save the undecoded data and the key context into the primitive + // value. + context := make(Key, len(md.context)) + copy(context, md.context) + rv.Set(reflect.ValueOf(Primitive{ + undecoded: data, + context: context, + })) + return nil + } + + rvi := rv.Interface() + if v, ok := rvi.(Unmarshaler); ok { + err := v.UnmarshalTOML(data) + if err != nil { + return md.parseErr(err) + } + // Assume the Unmarshaler decoded everything, so mark all keys under + // this table as decoded. + if tmap, ok := data.(map[string]any); ok { + markDecodedRecursive(md, tmap) + } + if aot, ok := data.([]map[string]any); ok { + for _, tmap := range aot { + markDecodedRecursive(md, tmap) + } + } + return nil + } + if v, ok := rvi.(encoding.TextUnmarshaler); ok { + return md.unifyText(data, v) + } + + // TODO: + // The behavior here is incorrect whenever a Go type satisfies the + // encoding.TextUnmarshaler interface but also corresponds to a TOML hash or + // array. In particular, the unmarshaler should only be applied to primitive + // TOML values. But at this point, it will be applied to all kinds of values + // and produce an incorrect error whenever those values are hashes or arrays + // (including arrays of tables). + + k := rv.Kind() + + if k >= reflect.Int && k <= reflect.Uint64 { + return md.unifyInt(data, rv) + } + switch k { + case reflect.Struct: + return md.unifyStruct(data, rv) + case reflect.Map: + return md.unifyMap(data, rv) + case reflect.Array: + return md.unifyArray(data, rv) + case reflect.Slice: + return md.unifySlice(data, rv) + case reflect.String: + return md.unifyString(data, rv) + case reflect.Bool: + return md.unifyBool(data, rv) + case reflect.Interface: + if rv.NumMethod() > 0 { /// Only empty interfaces are supported. + return md.e("unsupported type %s", rv.Type()) + } + return md.unifyAnything(data, rv) + case reflect.Float32, reflect.Float64: + return md.unifyFloat64(data, rv) + } + return md.e("unsupported type %s", rv.Kind()) +} + +func (md *MetaData) unifyStruct(mapping any, rv reflect.Value) error { + tmap, ok := mapping.(map[string]any) + if !ok { + if mapping == nil { + return nil + } + return md.e("type mismatch for %s: expected table but found %s", rv.Type().String(), fmtType(mapping)) + } + + for key, datum := range tmap { + var f *field + fields := cachedTypeFields(rv.Type()) + for i := range fields { + ff := &fields[i] + if ff.name == key { + f = ff + break + } + if f == nil && strings.EqualFold(ff.name, key) { + f = ff + } + } + if f != nil { + subv := rv + for _, i := range f.index { + subv = indirect(subv.Field(i)) + } + + if isUnifiable(subv) { + md.decoded[md.context.add(key).String()] = struct{}{} + md.context = append(md.context, key) + + err := md.unify(datum, subv) + if err != nil { + return err + } + md.context = md.context[0 : len(md.context)-1] + } else if f.name != "" { + return md.e("cannot write unexported field %s.%s", rv.Type().String(), f.name) + } + } + } + return nil +} + +func (md *MetaData) unifyMap(mapping any, rv reflect.Value) error { + keyType := rv.Type().Key().Kind() + if keyType != reflect.String && keyType != reflect.Interface { + return fmt.Errorf("toml: cannot decode to a map with non-string key type (%s in %q)", + keyType, rv.Type()) + } + + tmap, ok := mapping.(map[string]any) + if !ok { + if tmap == nil { + return nil + } + return md.badtype("map", mapping) + } + if rv.IsNil() { + rv.Set(reflect.MakeMap(rv.Type())) + } + for k, v := range tmap { + md.decoded[md.context.add(k).String()] = struct{}{} + md.context = append(md.context, k) + + rvval := reflect.Indirect(reflect.New(rv.Type().Elem())) + + err := md.unify(v, indirect(rvval)) + if err != nil { + return err + } + md.context = md.context[0 : len(md.context)-1] + + rvkey := indirect(reflect.New(rv.Type().Key())) + + switch keyType { + case reflect.Interface: + rvkey.Set(reflect.ValueOf(k)) + case reflect.String: + rvkey.SetString(k) + } + + rv.SetMapIndex(rvkey, rvval) + } + return nil +} + +func (md *MetaData) unifyArray(data any, rv reflect.Value) error { + datav := reflect.ValueOf(data) + if datav.Kind() != reflect.Slice { + if !datav.IsValid() { + return nil + } + return md.badtype("slice", data) + } + if l := datav.Len(); l != rv.Len() { + return md.e("expected array length %d; got TOML array of length %d", rv.Len(), l) + } + return md.unifySliceArray(datav, rv) +} + +func (md *MetaData) unifySlice(data any, rv reflect.Value) error { + datav := reflect.ValueOf(data) + if datav.Kind() != reflect.Slice { + if !datav.IsValid() { + return nil + } + return md.badtype("slice", data) + } + n := datav.Len() + if rv.IsNil() || rv.Cap() < n { + rv.Set(reflect.MakeSlice(rv.Type(), n, n)) + } + rv.SetLen(n) + return md.unifySliceArray(datav, rv) +} + +func (md *MetaData) unifySliceArray(data, rv reflect.Value) error { + l := data.Len() + for i := 0; i < l; i++ { + err := md.unify(data.Index(i).Interface(), indirect(rv.Index(i))) + if err != nil { + return err + } + } + return nil +} + +func (md *MetaData) unifyString(data any, rv reflect.Value) error { + _, ok := rv.Interface().(json.Number) + if ok { + if i, ok := data.(int64); ok { + rv.SetString(strconv.FormatInt(i, 10)) + } else if f, ok := data.(float64); ok { + rv.SetString(strconv.FormatFloat(f, 'f', -1, 64)) + } else { + return md.badtype("string", data) + } + return nil + } + + if s, ok := data.(string); ok { + rv.SetString(s) + return nil + } + return md.badtype("string", data) +} + +func (md *MetaData) unifyFloat64(data any, rv reflect.Value) error { + rvk := rv.Kind() + + if num, ok := data.(float64); ok { + switch rvk { + case reflect.Float32: + if num < -math.MaxFloat32 || num > math.MaxFloat32 { + return md.parseErr(errParseRange{i: num, size: rvk.String()}) + } + fallthrough + case reflect.Float64: + rv.SetFloat(num) + default: + panic("bug") + } + return nil + } + + if num, ok := data.(int64); ok { + if (rvk == reflect.Float32 && (num < -maxSafeFloat32Int || num > maxSafeFloat32Int)) || + (rvk == reflect.Float64 && (num < -maxSafeFloat64Int || num > maxSafeFloat64Int)) { + return md.parseErr(errUnsafeFloat{i: num, size: rvk.String()}) + } + rv.SetFloat(float64(num)) + return nil + } + + return md.badtype("float", data) +} + +func (md *MetaData) unifyInt(data any, rv reflect.Value) error { + _, ok := rv.Interface().(time.Duration) + if ok { + // Parse as string duration, and fall back to regular integer parsing + // (as nanosecond) if this is not a string. + if s, ok := data.(string); ok { + dur, err := time.ParseDuration(s) + if err != nil { + return md.parseErr(errParseDuration{s}) + } + rv.SetInt(int64(dur)) + return nil + } + } + + num, ok := data.(int64) + if !ok { + return md.badtype("integer", data) + } + + rvk := rv.Kind() + switch { + case rvk >= reflect.Int && rvk <= reflect.Int64: + if (rvk == reflect.Int8 && (num < math.MinInt8 || num > math.MaxInt8)) || + (rvk == reflect.Int16 && (num < math.MinInt16 || num > math.MaxInt16)) || + (rvk == reflect.Int32 && (num < math.MinInt32 || num > math.MaxInt32)) { + return md.parseErr(errParseRange{i: num, size: rvk.String()}) + } + rv.SetInt(num) + case rvk >= reflect.Uint && rvk <= reflect.Uint64: + unum := uint64(num) + if rvk == reflect.Uint8 && (num < 0 || unum > math.MaxUint8) || + rvk == reflect.Uint16 && (num < 0 || unum > math.MaxUint16) || + rvk == reflect.Uint32 && (num < 0 || unum > math.MaxUint32) { + return md.parseErr(errParseRange{i: num, size: rvk.String()}) + } + rv.SetUint(unum) + default: + panic("unreachable") + } + return nil +} + +func (md *MetaData) unifyBool(data any, rv reflect.Value) error { + if b, ok := data.(bool); ok { + rv.SetBool(b) + return nil + } + return md.badtype("boolean", data) +} + +func (md *MetaData) unifyAnything(data any, rv reflect.Value) error { + rv.Set(reflect.ValueOf(data)) + return nil +} + +func (md *MetaData) unifyText(data any, v encoding.TextUnmarshaler) error { + var s string + switch sdata := data.(type) { + case Marshaler: + text, err := sdata.MarshalTOML() + if err != nil { + return err + } + s = string(text) + case encoding.TextMarshaler: + text, err := sdata.MarshalText() + if err != nil { + return err + } + s = string(text) + case fmt.Stringer: + s = sdata.String() + case string: + s = sdata + case bool: + s = fmt.Sprintf("%v", sdata) + case int64: + s = fmt.Sprintf("%d", sdata) + case float64: + s = fmt.Sprintf("%f", sdata) + default: + return md.badtype("primitive (string-like)", data) + } + if err := v.UnmarshalText([]byte(s)); err != nil { + return md.parseErr(err) + } + return nil +} + +func (md *MetaData) badtype(dst string, data any) error { + return md.e("incompatible types: TOML value has type %s; destination has type %s", fmtType(data), dst) +} + +func (md *MetaData) parseErr(err error) error { + k := md.context.String() + d := string(md.data) + return ParseError{ + Message: err.Error(), + err: err, + LastKey: k, + Position: md.keyInfo[k].pos.withCol(d), + Line: md.keyInfo[k].pos.Line, + input: d, + } +} + +func (md *MetaData) e(format string, args ...any) error { + f := "toml: " + if len(md.context) > 0 { + f = fmt.Sprintf("toml: (last key %q): ", md.context) + p := md.keyInfo[md.context.String()].pos + if p.Line > 0 { + f = fmt.Sprintf("toml: line %d (last key %q): ", p.Line, md.context) + } + } + return fmt.Errorf(f+format, args...) +} + +// rvalue returns a reflect.Value of `v`. All pointers are resolved. +func rvalue(v any) reflect.Value { + return indirect(reflect.ValueOf(v)) +} + +// indirect returns the value pointed to by a pointer. +// +// Pointers are followed until the value is not a pointer. New values are +// allocated for each nil pointer. +// +// An exception to this rule is if the value satisfies an interface of interest +// to us (like encoding.TextUnmarshaler). +func indirect(v reflect.Value) reflect.Value { + if v.Kind() != reflect.Ptr { + if v.CanSet() { + pv := v.Addr() + pvi := pv.Interface() + if _, ok := pvi.(encoding.TextUnmarshaler); ok { + return pv + } + if _, ok := pvi.(Unmarshaler); ok { + return pv + } + } + return v + } + if v.IsNil() { + v.Set(reflect.New(v.Type().Elem())) + } + return indirect(reflect.Indirect(v)) +} + +func isUnifiable(rv reflect.Value) bool { + if rv.CanSet() { + return true + } + rvi := rv.Interface() + if _, ok := rvi.(encoding.TextUnmarshaler); ok { + return true + } + if _, ok := rvi.(Unmarshaler); ok { + return true + } + return false +} + +// fmt %T with "interface {}" replaced with "any", which is far more readable. +func fmtType(t any) string { + return strings.ReplaceAll(fmt.Sprintf("%T", t), "interface {}", "any") +} diff --git a/vendor/github.com/BurntSushi/toml/deprecated.go b/vendor/github.com/BurntSushi/toml/deprecated.go new file mode 100644 index 0000000..155709a --- /dev/null +++ b/vendor/github.com/BurntSushi/toml/deprecated.go @@ -0,0 +1,29 @@ +package toml + +import ( + "encoding" + "io" +) + +// TextMarshaler is an alias for encoding.TextMarshaler. +// +// Deprecated: use encoding.TextMarshaler +type TextMarshaler encoding.TextMarshaler + +// TextUnmarshaler is an alias for encoding.TextUnmarshaler. +// +// Deprecated: use encoding.TextUnmarshaler +type TextUnmarshaler encoding.TextUnmarshaler + +// DecodeReader is an alias for NewDecoder(r).Decode(v). +// +// Deprecated: use NewDecoder(reader).Decode(&value). +func DecodeReader(r io.Reader, v any) (MetaData, error) { return NewDecoder(r).Decode(v) } + +// PrimitiveDecode is an alias for MetaData.PrimitiveDecode(). +// +// Deprecated: use MetaData.PrimitiveDecode. +func PrimitiveDecode(primValue Primitive, v any) error { + md := MetaData{decoded: make(map[string]struct{})} + return md.unify(primValue.undecoded, rvalue(v)) +} diff --git a/vendor/github.com/BurntSushi/toml/doc.go b/vendor/github.com/BurntSushi/toml/doc.go new file mode 100644 index 0000000..82c90a9 --- /dev/null +++ b/vendor/github.com/BurntSushi/toml/doc.go @@ -0,0 +1,8 @@ +// Package toml implements decoding and encoding of TOML files. +// +// This package supports TOML v1.0.0, as specified at https://toml.io +// +// The github.com/BurntSushi/toml/cmd/tomlv package implements a TOML validator, +// and can be used to verify if TOML document is valid. It can also be used to +// print the type of each key. +package toml diff --git a/vendor/github.com/BurntSushi/toml/encode.go b/vendor/github.com/BurntSushi/toml/encode.go new file mode 100644 index 0000000..ac196e7 --- /dev/null +++ b/vendor/github.com/BurntSushi/toml/encode.go @@ -0,0 +1,776 @@ +package toml + +import ( + "bufio" + "bytes" + "encoding" + "encoding/json" + "errors" + "fmt" + "io" + "math" + "reflect" + "sort" + "strconv" + "strings" + "time" + + "github.com/BurntSushi/toml/internal" +) + +type tomlEncodeError struct{ error } + +var ( + errArrayNilElement = errors.New("toml: cannot encode array with nil element") + errNonString = errors.New("toml: cannot encode a map with non-string key type") + errNoKey = errors.New("toml: top-level values must be Go maps or structs") + errAnything = errors.New("") // used in testing +) + +var dblQuotedReplacer = strings.NewReplacer( + "\"", "\\\"", + "\\", "\\\\", + "\x00", `\u0000`, + "\x01", `\u0001`, + "\x02", `\u0002`, + "\x03", `\u0003`, + "\x04", `\u0004`, + "\x05", `\u0005`, + "\x06", `\u0006`, + "\x07", `\u0007`, + "\b", `\b`, + "\t", `\t`, + "\n", `\n`, + "\x0b", `\u000b`, + "\f", `\f`, + "\r", `\r`, + "\x0e", `\u000e`, + "\x0f", `\u000f`, + "\x10", `\u0010`, + "\x11", `\u0011`, + "\x12", `\u0012`, + "\x13", `\u0013`, + "\x14", `\u0014`, + "\x15", `\u0015`, + "\x16", `\u0016`, + "\x17", `\u0017`, + "\x18", `\u0018`, + "\x19", `\u0019`, + "\x1a", `\u001a`, + "\x1b", `\u001b`, + "\x1c", `\u001c`, + "\x1d", `\u001d`, + "\x1e", `\u001e`, + "\x1f", `\u001f`, + "\x7f", `\u007f`, +) + +var ( + marshalToml = reflect.TypeOf((*Marshaler)(nil)).Elem() + marshalText = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem() + timeType = reflect.TypeOf((*time.Time)(nil)).Elem() +) + +// Marshaler is the interface implemented by types that can marshal themselves +// into valid TOML. +type Marshaler interface { + MarshalTOML() ([]byte, error) +} + +// Marshal returns a TOML representation of the Go value. +// +// See [Encoder] for a description of the encoding process. +func Marshal(v any) ([]byte, error) { + buff := new(bytes.Buffer) + if err := NewEncoder(buff).Encode(v); err != nil { + return nil, err + } + return buff.Bytes(), nil +} + +// Encoder encodes a Go to a TOML document. +// +// The mapping between Go values and TOML values should be precisely the same as +// for [Decode]. +// +// time.Time is encoded as a RFC 3339 string, and time.Duration as its string +// representation. +// +// The [Marshaler] and [encoding.TextMarshaler] interfaces are supported to +// encoding the value as custom TOML. +// +// If you want to write arbitrary binary data then you will need to use +// something like base64 since TOML does not have any binary types. +// +// When encoding TOML hashes (Go maps or structs), keys without any sub-hashes +// are encoded first. +// +// Go maps will be sorted alphabetically by key for deterministic output. +// +// The toml struct tag can be used to provide the key name; if omitted the +// struct field name will be used. If the "omitempty" option is present the +// following value will be skipped: +// +// - arrays, slices, maps, and string with len of 0 +// - struct with all zero values +// - bool false +// +// If omitzero is given all int and float types with a value of 0 will be +// skipped. +// +// Encoding Go values without a corresponding TOML representation will return an +// error. Examples of this includes maps with non-string keys, slices with nil +// elements, embedded non-struct types, and nested slices containing maps or +// structs. (e.g. [][]map[string]string is not allowed but []map[string]string +// is okay, as is []map[string][]string). +// +// NOTE: only exported keys are encoded due to the use of reflection. Unexported +// keys are silently discarded. +type Encoder struct { + Indent string // string for a single indentation level; default is two spaces. + hasWritten bool // written any output to w yet? + w *bufio.Writer +} + +// NewEncoder create a new Encoder. +func NewEncoder(w io.Writer) *Encoder { + return &Encoder{w: bufio.NewWriter(w), Indent: " "} +} + +// Encode writes a TOML representation of the Go value to the [Encoder]'s writer. +// +// An error is returned if the value given cannot be encoded to a valid TOML +// document. +func (enc *Encoder) Encode(v any) error { + rv := eindirect(reflect.ValueOf(v)) + err := enc.safeEncode(Key([]string{}), rv) + if err != nil { + return err + } + return enc.w.Flush() +} + +func (enc *Encoder) safeEncode(key Key, rv reflect.Value) (err error) { + defer func() { + if r := recover(); r != nil { + if terr, ok := r.(tomlEncodeError); ok { + err = terr.error + return + } + panic(r) + } + }() + enc.encode(key, rv) + return nil +} + +func (enc *Encoder) encode(key Key, rv reflect.Value) { + // If we can marshal the type to text, then we use that. This prevents the + // encoder for handling these types as generic structs (or whatever the + // underlying type of a TextMarshaler is). + switch { + case isMarshaler(rv): + enc.writeKeyValue(key, rv, false) + return + case rv.Type() == primitiveType: // TODO: #76 would make this superfluous after implemented. + enc.encode(key, reflect.ValueOf(rv.Interface().(Primitive).undecoded)) + return + } + + k := rv.Kind() + switch k { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, + reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, + reflect.Uint64, + reflect.Float32, reflect.Float64, reflect.String, reflect.Bool: + enc.writeKeyValue(key, rv, false) + case reflect.Array, reflect.Slice: + if typeEqual(tomlArrayHash, tomlTypeOfGo(rv)) { + enc.eArrayOfTables(key, rv) + } else { + enc.writeKeyValue(key, rv, false) + } + case reflect.Interface: + if rv.IsNil() { + return + } + enc.encode(key, rv.Elem()) + case reflect.Map: + if rv.IsNil() { + return + } + enc.eTable(key, rv) + case reflect.Ptr: + if rv.IsNil() { + return + } + enc.encode(key, rv.Elem()) + case reflect.Struct: + enc.eTable(key, rv) + default: + encPanic(fmt.Errorf("unsupported type for key '%s': %s", key, k)) + } +} + +// eElement encodes any value that can be an array element. +func (enc *Encoder) eElement(rv reflect.Value) { + switch v := rv.Interface().(type) { + case time.Time: // Using TextMarshaler adds extra quotes, which we don't want. + format := time.RFC3339Nano + switch v.Location() { + case internal.LocalDatetime: + format = "2006-01-02T15:04:05.999999999" + case internal.LocalDate: + format = "2006-01-02" + case internal.LocalTime: + format = "15:04:05.999999999" + } + switch v.Location() { + default: + enc.wf(v.Format(format)) + case internal.LocalDatetime, internal.LocalDate, internal.LocalTime: + enc.wf(v.In(time.UTC).Format(format)) + } + return + case Marshaler: + s, err := v.MarshalTOML() + if err != nil { + encPanic(err) + } + if s == nil { + encPanic(errors.New("MarshalTOML returned nil and no error")) + } + enc.w.Write(s) + return + case encoding.TextMarshaler: + s, err := v.MarshalText() + if err != nil { + encPanic(err) + } + if s == nil { + encPanic(errors.New("MarshalText returned nil and no error")) + } + enc.writeQuoted(string(s)) + return + case time.Duration: + enc.writeQuoted(v.String()) + return + case json.Number: + n, _ := rv.Interface().(json.Number) + + if n == "" { /// Useful zero value. + enc.w.WriteByte('0') + return + } else if v, err := n.Int64(); err == nil { + enc.eElement(reflect.ValueOf(v)) + return + } else if v, err := n.Float64(); err == nil { + enc.eElement(reflect.ValueOf(v)) + return + } + encPanic(fmt.Errorf("unable to convert %q to int64 or float64", n)) + } + + switch rv.Kind() { + case reflect.Ptr: + enc.eElement(rv.Elem()) + return + case reflect.String: + enc.writeQuoted(rv.String()) + case reflect.Bool: + enc.wf(strconv.FormatBool(rv.Bool())) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + enc.wf(strconv.FormatInt(rv.Int(), 10)) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + enc.wf(strconv.FormatUint(rv.Uint(), 10)) + case reflect.Float32: + f := rv.Float() + if math.IsNaN(f) { + if math.Signbit(f) { + enc.wf("-") + } + enc.wf("nan") + } else if math.IsInf(f, 0) { + if math.Signbit(f) { + enc.wf("-") + } + enc.wf("inf") + } else { + enc.wf(floatAddDecimal(strconv.FormatFloat(f, 'f', -1, 32))) + } + case reflect.Float64: + f := rv.Float() + if math.IsNaN(f) { + if math.Signbit(f) { + enc.wf("-") + } + enc.wf("nan") + } else if math.IsInf(f, 0) { + if math.Signbit(f) { + enc.wf("-") + } + enc.wf("inf") + } else { + enc.wf(floatAddDecimal(strconv.FormatFloat(f, 'f', -1, 64))) + } + case reflect.Array, reflect.Slice: + enc.eArrayOrSliceElement(rv) + case reflect.Struct: + enc.eStruct(nil, rv, true) + case reflect.Map: + enc.eMap(nil, rv, true) + case reflect.Interface: + enc.eElement(rv.Elem()) + default: + encPanic(fmt.Errorf("unexpected type: %s", fmtType(rv.Interface()))) + } +} + +// By the TOML spec, all floats must have a decimal with at least one number on +// either side. +func floatAddDecimal(fstr string) string { + if !strings.Contains(fstr, ".") { + return fstr + ".0" + } + return fstr +} + +func (enc *Encoder) writeQuoted(s string) { + enc.wf("\"%s\"", dblQuotedReplacer.Replace(s)) +} + +func (enc *Encoder) eArrayOrSliceElement(rv reflect.Value) { + length := rv.Len() + enc.wf("[") + for i := 0; i < length; i++ { + elem := eindirect(rv.Index(i)) + enc.eElement(elem) + if i != length-1 { + enc.wf(", ") + } + } + enc.wf("]") +} + +func (enc *Encoder) eArrayOfTables(key Key, rv reflect.Value) { + if len(key) == 0 { + encPanic(errNoKey) + } + for i := 0; i < rv.Len(); i++ { + trv := eindirect(rv.Index(i)) + if isNil(trv) { + continue + } + enc.newline() + enc.wf("%s[[%s]]", enc.indentStr(key), key) + enc.newline() + enc.eMapOrStruct(key, trv, false) + } +} + +func (enc *Encoder) eTable(key Key, rv reflect.Value) { + if len(key) == 1 { + // Output an extra newline between top-level tables. + // (The newline isn't written if nothing else has been written though.) + enc.newline() + } + if len(key) > 0 { + enc.wf("%s[%s]", enc.indentStr(key), key) + enc.newline() + } + enc.eMapOrStruct(key, rv, false) +} + +func (enc *Encoder) eMapOrStruct(key Key, rv reflect.Value, inline bool) { + switch rv.Kind() { + case reflect.Map: + enc.eMap(key, rv, inline) + case reflect.Struct: + enc.eStruct(key, rv, inline) + default: + // Should never happen? + panic("eTable: unhandled reflect.Value Kind: " + rv.Kind().String()) + } +} + +func (enc *Encoder) eMap(key Key, rv reflect.Value, inline bool) { + rt := rv.Type() + if rt.Key().Kind() != reflect.String { + encPanic(errNonString) + } + + // Sort keys so that we have deterministic output. And write keys directly + // underneath this key first, before writing sub-structs or sub-maps. + var mapKeysDirect, mapKeysSub []reflect.Value + for _, mapKey := range rv.MapKeys() { + if typeIsTable(tomlTypeOfGo(eindirect(rv.MapIndex(mapKey)))) { + mapKeysSub = append(mapKeysSub, mapKey) + } else { + mapKeysDirect = append(mapKeysDirect, mapKey) + } + } + + writeMapKeys := func(mapKeys []reflect.Value, trailC bool) { + sort.Slice(mapKeys, func(i, j int) bool { return mapKeys[i].String() < mapKeys[j].String() }) + for i, mapKey := range mapKeys { + val := eindirect(rv.MapIndex(mapKey)) + if isNil(val) { + continue + } + + if inline { + enc.writeKeyValue(Key{mapKey.String()}, val, true) + if trailC || i != len(mapKeys)-1 { + enc.wf(", ") + } + } else { + enc.encode(key.add(mapKey.String()), val) + } + } + } + + if inline { + enc.wf("{") + } + writeMapKeys(mapKeysDirect, len(mapKeysSub) > 0) + writeMapKeys(mapKeysSub, false) + if inline { + enc.wf("}") + } +} + +func pointerTo(t reflect.Type) reflect.Type { + if t.Kind() == reflect.Ptr { + return pointerTo(t.Elem()) + } + return t +} + +func (enc *Encoder) eStruct(key Key, rv reflect.Value, inline bool) { + // Write keys for fields directly under this key first, because if we write + // a field that creates a new table then all keys under it will be in that + // table (not the one we're writing here). + // + // Fields is a [][]int: for fieldsDirect this always has one entry (the + // struct index). For fieldsSub it contains two entries: the parent field + // index from tv, and the field indexes for the fields of the sub. + var ( + rt = rv.Type() + fieldsDirect, fieldsSub [][]int + addFields func(rt reflect.Type, rv reflect.Value, start []int) + ) + addFields = func(rt reflect.Type, rv reflect.Value, start []int) { + for i := 0; i < rt.NumField(); i++ { + f := rt.Field(i) + isEmbed := f.Anonymous && pointerTo(f.Type).Kind() == reflect.Struct + if f.PkgPath != "" && !isEmbed { /// Skip unexported fields. + continue + } + opts := getOptions(f.Tag) + if opts.skip { + continue + } + + frv := eindirect(rv.Field(i)) + + // Need to make a copy because ... ehm, I don't know why... I guess + // allocating a new array can cause it to fail(?) + // + // Done for: https://github.com/BurntSushi/toml/issues/430 + // Previously only on 32bit for: https://github.com/BurntSushi/toml/issues/314 + copyStart := make([]int, len(start)) + copy(copyStart, start) + start = copyStart + + // Treat anonymous struct fields with tag names as though they are + // not anonymous, like encoding/json does. + // + // Non-struct anonymous fields use the normal encoding logic. + if isEmbed { + if getOptions(f.Tag).name == "" && frv.Kind() == reflect.Struct { + addFields(frv.Type(), frv, append(start, f.Index...)) + continue + } + } + + if typeIsTable(tomlTypeOfGo(frv)) { + fieldsSub = append(fieldsSub, append(start, f.Index...)) + } else { + fieldsDirect = append(fieldsDirect, append(start, f.Index...)) + } + } + } + addFields(rt, rv, nil) + + writeFields := func(fields [][]int, totalFields int) { + for _, fieldIndex := range fields { + fieldType := rt.FieldByIndex(fieldIndex) + fieldVal := rv.FieldByIndex(fieldIndex) + + opts := getOptions(fieldType.Tag) + if opts.skip { + continue + } + if opts.omitempty && isEmpty(fieldVal) { + continue + } + + fieldVal = eindirect(fieldVal) + + if isNil(fieldVal) { /// Don't write anything for nil fields. + continue + } + + keyName := fieldType.Name + if opts.name != "" { + keyName = opts.name + } + + if opts.omitzero && isZero(fieldVal) { + continue + } + + if inline { + enc.writeKeyValue(Key{keyName}, fieldVal, true) + if fieldIndex[0] != totalFields-1 { + enc.wf(", ") + } + } else { + enc.encode(key.add(keyName), fieldVal) + } + } + } + + if inline { + enc.wf("{") + } + + l := len(fieldsDirect) + len(fieldsSub) + writeFields(fieldsDirect, l) + writeFields(fieldsSub, l) + if inline { + enc.wf("}") + } +} + +// tomlTypeOfGo returns the TOML type name of the Go value's type. +// +// It is used to determine whether the types of array elements are mixed (which +// is forbidden). If the Go value is nil, then it is illegal for it to be an +// array element, and valueIsNil is returned as true. +// +// The type may be `nil`, which means no concrete TOML type could be found. +func tomlTypeOfGo(rv reflect.Value) tomlType { + if isNil(rv) || !rv.IsValid() { + return nil + } + + if rv.Kind() == reflect.Struct { + if rv.Type() == timeType { + return tomlDatetime + } + if isMarshaler(rv) { + return tomlString + } + return tomlHash + } + + if isMarshaler(rv) { + return tomlString + } + + switch rv.Kind() { + case reflect.Bool: + return tomlBool + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, + reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, + reflect.Uint64: + return tomlInteger + case reflect.Float32, reflect.Float64: + return tomlFloat + case reflect.Array, reflect.Slice: + if isTableArray(rv) { + return tomlArrayHash + } + return tomlArray + case reflect.Ptr, reflect.Interface: + return tomlTypeOfGo(rv.Elem()) + case reflect.String: + return tomlString + case reflect.Map: + return tomlHash + default: + encPanic(errors.New("unsupported type: " + rv.Kind().String())) + panic("unreachable") + } +} + +func isMarshaler(rv reflect.Value) bool { + return rv.Type().Implements(marshalText) || rv.Type().Implements(marshalToml) +} + +// isTableArray reports if all entries in the array or slice are a table. +func isTableArray(arr reflect.Value) bool { + if isNil(arr) || !arr.IsValid() || arr.Len() == 0 { + return false + } + + ret := true + for i := 0; i < arr.Len(); i++ { + tt := tomlTypeOfGo(eindirect(arr.Index(i))) + // Don't allow nil. + if tt == nil { + encPanic(errArrayNilElement) + } + + if ret && !typeEqual(tomlHash, tt) { + ret = false + } + } + return ret +} + +type tagOptions struct { + skip bool // "-" + name string + omitempty bool + omitzero bool +} + +func getOptions(tag reflect.StructTag) tagOptions { + t := tag.Get("toml") + if t == "-" { + return tagOptions{skip: true} + } + var opts tagOptions + parts := strings.Split(t, ",") + opts.name = parts[0] + for _, s := range parts[1:] { + switch s { + case "omitempty": + opts.omitempty = true + case "omitzero": + opts.omitzero = true + } + } + return opts +} + +func isZero(rv reflect.Value) bool { + switch rv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return rv.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return rv.Uint() == 0 + case reflect.Float32, reflect.Float64: + return rv.Float() == 0.0 + } + return false +} + +func isEmpty(rv reflect.Value) bool { + switch rv.Kind() { + case reflect.Array, reflect.Slice, reflect.Map, reflect.String: + return rv.Len() == 0 + case reflect.Struct: + if rv.Type().Comparable() { + return reflect.Zero(rv.Type()).Interface() == rv.Interface() + } + // Need to also check if all the fields are empty, otherwise something + // like this with uncomparable types will always return true: + // + // type a struct{ field b } + // type b struct{ s []string } + // s := a{field: b{s: []string{"AAA"}}} + for i := 0; i < rv.NumField(); i++ { + if !isEmpty(rv.Field(i)) { + return false + } + } + return true + case reflect.Bool: + return !rv.Bool() + case reflect.Ptr: + return rv.IsNil() + } + return false +} + +func (enc *Encoder) newline() { + if enc.hasWritten { + enc.wf("\n") + } +} + +// Write a key/value pair: +// +// key = +// +// This is also used for "k = v" in inline tables; so something like this will +// be written in three calls: +// +// ┌───────────────────┐ +// │ ┌───┐ ┌────┐│ +// v v v v vv +// key = {k = 1, k2 = 2} +func (enc *Encoder) writeKeyValue(key Key, val reflect.Value, inline bool) { + /// Marshaler used on top-level document; call eElement() to just call + /// Marshal{TOML,Text}. + if len(key) == 0 { + enc.eElement(val) + return + } + enc.wf("%s%s = ", enc.indentStr(key), key.maybeQuoted(len(key)-1)) + enc.eElement(val) + if !inline { + enc.newline() + } +} + +func (enc *Encoder) wf(format string, v ...any) { + _, err := fmt.Fprintf(enc.w, format, v...) + if err != nil { + encPanic(err) + } + enc.hasWritten = true +} + +func (enc *Encoder) indentStr(key Key) string { + return strings.Repeat(enc.Indent, len(key)-1) +} + +func encPanic(err error) { + panic(tomlEncodeError{err}) +} + +// Resolve any level of pointers to the actual value (e.g. **string → string). +func eindirect(v reflect.Value) reflect.Value { + if v.Kind() != reflect.Ptr && v.Kind() != reflect.Interface { + if isMarshaler(v) { + return v + } + if v.CanAddr() { /// Special case for marshalers; see #358. + if pv := v.Addr(); isMarshaler(pv) { + return pv + } + } + return v + } + + if v.IsNil() { + return v + } + + return eindirect(v.Elem()) +} + +func isNil(rv reflect.Value) bool { + switch rv.Kind() { + case reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: + return rv.IsNil() + default: + return false + } +} diff --git a/vendor/github.com/BurntSushi/toml/error.go b/vendor/github.com/BurntSushi/toml/error.go new file mode 100644 index 0000000..b7077d3 --- /dev/null +++ b/vendor/github.com/BurntSushi/toml/error.go @@ -0,0 +1,347 @@ +package toml + +import ( + "fmt" + "strings" +) + +// ParseError is returned when there is an error parsing the TOML syntax such as +// invalid syntax, duplicate keys, etc. +// +// In addition to the error message itself, you can also print detailed location +// information with context by using [ErrorWithPosition]: +// +// toml: error: Key 'fruit' was already created and cannot be used as an array. +// +// At line 4, column 2-7: +// +// 2 | fruit = [] +// 3 | +// 4 | [[fruit]] # Not allowed +// ^^^^^ +// +// [ErrorWithUsage] can be used to print the above with some more detailed usage +// guidance: +// +// toml: error: newlines not allowed within inline tables +// +// At line 1, column 18: +// +// 1 | x = [{ key = 42 # +// ^ +// +// Error help: +// +// Inline tables must always be on a single line: +// +// table = {key = 42, second = 43} +// +// It is invalid to split them over multiple lines like so: +// +// # INVALID +// table = { +// key = 42, +// second = 43 +// } +// +// Use regular for this: +// +// [table] +// key = 42 +// second = 43 +type ParseError struct { + Message string // Short technical message. + Usage string // Longer message with usage guidance; may be blank. + Position Position // Position of the error + LastKey string // Last parsed key, may be blank. + + // Line the error occurred. + // + // Deprecated: use [Position]. + Line int + + err error + input string +} + +// Position of an error. +type Position struct { + Line int // Line number, starting at 1. + Col int // Error column, starting at 1. + Start int // Start of error, as byte offset starting at 0. + Len int // Length of the error in bytes. +} + +func (p Position) withCol(tomlFile string) Position { + var ( + pos int + lines = strings.Split(tomlFile, "\n") + ) + for i := range lines { + ll := len(lines[i]) + 1 // +1 for the removed newline + if pos+ll >= p.Start { + p.Col = p.Start - pos + 1 + if p.Col < 1 { // Should never happen, but just in case. + p.Col = 1 + } + break + } + pos += ll + } + return p +} + +func (pe ParseError) Error() string { + if pe.LastKey == "" { + return fmt.Sprintf("toml: line %d: %s", pe.Position.Line, pe.Message) + } + return fmt.Sprintf("toml: line %d (last key %q): %s", + pe.Position.Line, pe.LastKey, pe.Message) +} + +// ErrorWithPosition returns the error with detailed location context. +// +// See the documentation on [ParseError]. +func (pe ParseError) ErrorWithPosition() string { + if pe.input == "" { // Should never happen, but just in case. + return pe.Error() + } + + // TODO: don't show control characters as literals? This may not show up + // well everywhere. + + var ( + lines = strings.Split(pe.input, "\n") + b = new(strings.Builder) + ) + if pe.Position.Len == 1 { + fmt.Fprintf(b, "toml: error: %s\n\nAt line %d, column %d:\n\n", + pe.Message, pe.Position.Line, pe.Position.Col) + } else { + fmt.Fprintf(b, "toml: error: %s\n\nAt line %d, column %d-%d:\n\n", + pe.Message, pe.Position.Line, pe.Position.Col, pe.Position.Col+pe.Position.Len-1) + } + if pe.Position.Line > 2 { + fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line-2, expandTab(lines[pe.Position.Line-3])) + } + if pe.Position.Line > 1 { + fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line-1, expandTab(lines[pe.Position.Line-2])) + } + + /// Expand tabs, so that the ^^^s are at the correct position, but leave + /// "column 10-13" intact. Adjusting this to the visual column would be + /// better, but we don't know the tabsize of the user in their editor, which + /// can be 8, 4, 2, or something else. We can't know. So leaving it as the + /// character index is probably the "most correct". + expanded := expandTab(lines[pe.Position.Line-1]) + diff := len(expanded) - len(lines[pe.Position.Line-1]) + + fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line, expanded) + fmt.Fprintf(b, "% 10s%s%s\n", "", strings.Repeat(" ", pe.Position.Col-1+diff), strings.Repeat("^", pe.Position.Len)) + return b.String() +} + +// ErrorWithUsage returns the error with detailed location context and usage +// guidance. +// +// See the documentation on [ParseError]. +func (pe ParseError) ErrorWithUsage() string { + m := pe.ErrorWithPosition() + if u, ok := pe.err.(interface{ Usage() string }); ok && u.Usage() != "" { + lines := strings.Split(strings.TrimSpace(u.Usage()), "\n") + for i := range lines { + if lines[i] != "" { + lines[i] = " " + lines[i] + } + } + return m + "Error help:\n\n" + strings.Join(lines, "\n") + "\n" + } + return m +} + +func expandTab(s string) string { + var ( + b strings.Builder + l int + fill = func(n int) string { + b := make([]byte, n) + for i := range b { + b[i] = ' ' + } + return string(b) + } + ) + b.Grow(len(s)) + for _, r := range s { + switch r { + case '\t': + tw := 8 - l%8 + b.WriteString(fill(tw)) + l += tw + default: + b.WriteRune(r) + l += 1 + } + } + return b.String() +} + +type ( + errLexControl struct{ r rune } + errLexEscape struct{ r rune } + errLexUTF8 struct{ b byte } + errParseDate struct{ v string } + errLexInlineTableNL struct{} + errLexStringNL struct{} + errParseRange struct { + i any // int or float + size string // "int64", "uint16", etc. + } + errUnsafeFloat struct { + i interface{} // float32 or float64 + size string // "float32" or "float64" + } + errParseDuration struct{ d string } +) + +func (e errLexControl) Error() string { + return fmt.Sprintf("TOML files cannot contain control characters: '0x%02x'", e.r) +} +func (e errLexControl) Usage() string { return "" } + +func (e errLexEscape) Error() string { return fmt.Sprintf(`invalid escape in string '\%c'`, e.r) } +func (e errLexEscape) Usage() string { return usageEscape } +func (e errLexUTF8) Error() string { return fmt.Sprintf("invalid UTF-8 byte: 0x%02x", e.b) } +func (e errLexUTF8) Usage() string { return "" } +func (e errParseDate) Error() string { return fmt.Sprintf("invalid datetime: %q", e.v) } +func (e errParseDate) Usage() string { return usageDate } +func (e errLexInlineTableNL) Error() string { return "newlines not allowed within inline tables" } +func (e errLexInlineTableNL) Usage() string { return usageInlineNewline } +func (e errLexStringNL) Error() string { return "strings cannot contain newlines" } +func (e errLexStringNL) Usage() string { return usageStringNewline } +func (e errParseRange) Error() string { return fmt.Sprintf("%v is out of range for %s", e.i, e.size) } +func (e errParseRange) Usage() string { return usageIntOverflow } +func (e errUnsafeFloat) Error() string { + return fmt.Sprintf("%v is out of the safe %s range", e.i, e.size) +} +func (e errUnsafeFloat) Usage() string { return usageUnsafeFloat } +func (e errParseDuration) Error() string { return fmt.Sprintf("invalid duration: %q", e.d) } +func (e errParseDuration) Usage() string { return usageDuration } + +const usageEscape = ` +A '\' inside a "-delimited string is interpreted as an escape character. + +The following escape sequences are supported: +\b, \t, \n, \f, \r, \", \\, \uXXXX, and \UXXXXXXXX + +To prevent a '\' from being recognized as an escape character, use either: + +- a ' or '''-delimited string; escape characters aren't processed in them; or +- write two backslashes to get a single backslash: '\\'. + +If you're trying to add a Windows path (e.g. "C:\Users\martin") then using '/' +instead of '\' will usually also work: "C:/Users/martin". +` + +const usageInlineNewline = ` +Inline tables must always be on a single line: + + table = {key = 42, second = 43} + +It is invalid to split them over multiple lines like so: + + # INVALID + table = { + key = 42, + second = 43 + } + +Use regular for this: + + [table] + key = 42 + second = 43 +` + +const usageStringNewline = ` +Strings must always be on a single line, and cannot span more than one line: + + # INVALID + string = "Hello, + world!" + +Instead use """ or ''' to split strings over multiple lines: + + string = """Hello, + world!""" +` + +const usageIntOverflow = ` +This number is too large; this may be an error in the TOML, but it can also be a +bug in the program that uses too small of an integer. + +The maximum and minimum values are: + + size │ lowest │ highest + ───────┼────────────────┼────────────── + int8 │ -128 │ 127 + int16 │ -32,768 │ 32,767 + int32 │ -2,147,483,648 │ 2,147,483,647 + int64 │ -9.2 × 10¹⁷ │ 9.2 × 10¹⁷ + uint8 │ 0 │ 255 + uint16 │ 0 │ 65,535 + uint32 │ 0 │ 4,294,967,295 + uint64 │ 0 │ 1.8 × 10¹⁸ + +int refers to int32 on 32-bit systems and int64 on 64-bit systems. +` + +const usageUnsafeFloat = ` +This number is outside of the "safe" range for floating point numbers; whole +(non-fractional) numbers outside the below range can not always be represented +accurately in a float, leading to some loss of accuracy. + +Explicitly mark a number as a fractional unit by adding ".0", which will incur +some loss of accuracy; for example: + + f = 2_000_000_000.0 + +Accuracy ranges: + + float32 = 16,777,215 + float64 = 9,007,199,254,740,991 +` + +const usageDuration = ` +A duration must be as "number", without any spaces. Valid units are: + + ns nanoseconds (billionth of a second) + us, µs microseconds (millionth of a second) + ms milliseconds (thousands of a second) + s seconds + m minutes + h hours + +You can combine multiple units; for example "5m10s" for 5 minutes and 10 +seconds. +` + +const usageDate = ` +A TOML datetime must be in one of the following formats: + + 2006-01-02T15:04:05Z07:00 Date and time, with timezone. + 2006-01-02T15:04:05 Date and time, but without timezone. + 2006-01-02 Date without a time or timezone. + 15:04:05 Just a time, without any timezone. + +Seconds may optionally have a fraction, up to nanosecond precision: + + 15:04:05.123 + 15:04:05.856018510 +` + +// TOML 1.1: +// The seconds part in times is optional, and may be omitted: +// 2006-01-02T15:04Z07:00 +// 2006-01-02T15:04 +// 15:04 diff --git a/vendor/github.com/BurntSushi/toml/internal/tz.go b/vendor/github.com/BurntSushi/toml/internal/tz.go new file mode 100644 index 0000000..022f15b --- /dev/null +++ b/vendor/github.com/BurntSushi/toml/internal/tz.go @@ -0,0 +1,36 @@ +package internal + +import "time" + +// Timezones used for local datetime, date, and time TOML types. +// +// The exact way times and dates without a timezone should be interpreted is not +// well-defined in the TOML specification and left to the implementation. These +// defaults to current local timezone offset of the computer, but this can be +// changed by changing these variables before decoding. +// +// TODO: +// Ideally we'd like to offer people the ability to configure the used timezone +// by setting Decoder.Timezone and Encoder.Timezone; however, this is a bit +// tricky: the reason we use three different variables for this is to support +// round-tripping – without these specific TZ names we wouldn't know which +// format to use. +// +// There isn't a good way to encode this right now though, and passing this sort +// of information also ties in to various related issues such as string format +// encoding, encoding of comments, etc. +// +// So, for the time being, just put this in internal until we can write a good +// comprehensive API for doing all of this. +// +// The reason they're exported is because they're referred from in e.g. +// internal/tag. +// +// Note that this behaviour is valid according to the TOML spec as the exact +// behaviour is left up to implementations. +var ( + localOffset = func() int { _, o := time.Now().Zone(); return o }() + LocalDatetime = time.FixedZone("datetime-local", localOffset) + LocalDate = time.FixedZone("date-local", localOffset) + LocalTime = time.FixedZone("time-local", localOffset) +) diff --git a/vendor/github.com/BurntSushi/toml/lex.go b/vendor/github.com/BurntSushi/toml/lex.go new file mode 100644 index 0000000..1c3b477 --- /dev/null +++ b/vendor/github.com/BurntSushi/toml/lex.go @@ -0,0 +1,1272 @@ +package toml + +import ( + "fmt" + "reflect" + "runtime" + "strings" + "unicode" + "unicode/utf8" +) + +type itemType int + +const ( + itemError itemType = iota + itemNIL // used in the parser to indicate no type + itemEOF + itemText + itemString + itemStringEsc + itemRawString + itemMultilineString + itemRawMultilineString + itemBool + itemInteger + itemFloat + itemDatetime + itemArray // the start of an array + itemArrayEnd + itemTableStart + itemTableEnd + itemArrayTableStart + itemArrayTableEnd + itemKeyStart + itemKeyEnd + itemCommentStart + itemInlineTableStart + itemInlineTableEnd +) + +const eof = 0 + +type stateFn func(lx *lexer) stateFn + +func (p Position) String() string { + return fmt.Sprintf("at line %d; start %d; length %d", p.Line, p.Start, p.Len) +} + +type lexer struct { + input string + start int + pos int + line int + state stateFn + items chan item + tomlNext bool + esc bool + + // Allow for backing up up to 4 runes. This is necessary because TOML + // contains 3-rune tokens (""" and '''). + prevWidths [4]int + nprev int // how many of prevWidths are in use + atEOF bool // If we emit an eof, we can still back up, but it is not OK to call next again. + + // A stack of state functions used to maintain context. + // + // The idea is to reuse parts of the state machine in various places. For + // example, values can appear at the top level or within arbitrarily nested + // arrays. The last state on the stack is used after a value has been lexed. + // Similarly for comments. + stack []stateFn +} + +type item struct { + typ itemType + val string + err error + pos Position +} + +func (lx *lexer) nextItem() item { + for { + select { + case item := <-lx.items: + return item + default: + lx.state = lx.state(lx) + //fmt.Printf(" STATE %-24s current: %-10s stack: %s\n", lx.state, lx.current(), lx.stack) + } + } +} + +func lex(input string, tomlNext bool) *lexer { + lx := &lexer{ + input: input, + state: lexTop, + items: make(chan item, 10), + stack: make([]stateFn, 0, 10), + line: 1, + tomlNext: tomlNext, + } + return lx +} + +func (lx *lexer) push(state stateFn) { + lx.stack = append(lx.stack, state) +} + +func (lx *lexer) pop() stateFn { + if len(lx.stack) == 0 { + return lx.errorf("BUG in lexer: no states to pop") + } + last := lx.stack[len(lx.stack)-1] + lx.stack = lx.stack[0 : len(lx.stack)-1] + return last +} + +func (lx *lexer) current() string { + return lx.input[lx.start:lx.pos] +} + +func (lx lexer) getPos() Position { + p := Position{ + Line: lx.line, + Start: lx.start, + Len: lx.pos - lx.start, + } + if p.Len <= 0 { + p.Len = 1 + } + return p +} + +func (lx *lexer) emit(typ itemType) { + // Needed for multiline strings ending with an incomplete UTF-8 sequence. + if lx.start > lx.pos { + lx.error(errLexUTF8{lx.input[lx.pos]}) + return + } + lx.items <- item{typ: typ, pos: lx.getPos(), val: lx.current()} + lx.start = lx.pos +} + +func (lx *lexer) emitTrim(typ itemType) { + lx.items <- item{typ: typ, pos: lx.getPos(), val: strings.TrimSpace(lx.current())} + lx.start = lx.pos +} + +func (lx *lexer) next() (r rune) { + if lx.atEOF { + panic("BUG in lexer: next called after EOF") + } + if lx.pos >= len(lx.input) { + lx.atEOF = true + return eof + } + + if lx.input[lx.pos] == '\n' { + lx.line++ + } + lx.prevWidths[3] = lx.prevWidths[2] + lx.prevWidths[2] = lx.prevWidths[1] + lx.prevWidths[1] = lx.prevWidths[0] + if lx.nprev < 4 { + lx.nprev++ + } + + r, w := utf8.DecodeRuneInString(lx.input[lx.pos:]) + if r == utf8.RuneError && w == 1 { + lx.error(errLexUTF8{lx.input[lx.pos]}) + return utf8.RuneError + } + + // Note: don't use peek() here, as this calls next(). + if isControl(r) || (r == '\r' && (len(lx.input)-1 == lx.pos || lx.input[lx.pos+1] != '\n')) { + lx.errorControlChar(r) + return utf8.RuneError + } + + lx.prevWidths[0] = w + lx.pos += w + return r +} + +// ignore skips over the pending input before this point. +func (lx *lexer) ignore() { + lx.start = lx.pos +} + +// backup steps back one rune. Can be called 4 times between calls to next. +func (lx *lexer) backup() { + if lx.atEOF { + lx.atEOF = false + return + } + if lx.nprev < 1 { + panic("BUG in lexer: backed up too far") + } + w := lx.prevWidths[0] + lx.prevWidths[0] = lx.prevWidths[1] + lx.prevWidths[1] = lx.prevWidths[2] + lx.prevWidths[2] = lx.prevWidths[3] + lx.nprev-- + + lx.pos -= w + if lx.pos < len(lx.input) && lx.input[lx.pos] == '\n' { + lx.line-- + } +} + +// accept consumes the next rune if it's equal to `valid`. +func (lx *lexer) accept(valid rune) bool { + if lx.next() == valid { + return true + } + lx.backup() + return false +} + +// peek returns but does not consume the next rune in the input. +func (lx *lexer) peek() rune { + r := lx.next() + lx.backup() + return r +} + +// skip ignores all input that matches the given predicate. +func (lx *lexer) skip(pred func(rune) bool) { + for { + r := lx.next() + if pred(r) { + continue + } + lx.backup() + lx.ignore() + return + } +} + +// error stops all lexing by emitting an error and returning `nil`. +// +// Note that any value that is a character is escaped if it's a special +// character (newlines, tabs, etc.). +func (lx *lexer) error(err error) stateFn { + if lx.atEOF { + return lx.errorPrevLine(err) + } + lx.items <- item{typ: itemError, pos: lx.getPos(), err: err} + return nil +} + +// errorfPrevline is like error(), but sets the position to the last column of +// the previous line. +// +// This is so that unexpected EOF or NL errors don't show on a new blank line. +func (lx *lexer) errorPrevLine(err error) stateFn { + pos := lx.getPos() + pos.Line-- + pos.Len = 1 + pos.Start = lx.pos - 1 + lx.items <- item{typ: itemError, pos: pos, err: err} + return nil +} + +// errorPos is like error(), but allows explicitly setting the position. +func (lx *lexer) errorPos(start, length int, err error) stateFn { + pos := lx.getPos() + pos.Start = start + pos.Len = length + lx.items <- item{typ: itemError, pos: pos, err: err} + return nil +} + +// errorf is like error, and creates a new error. +func (lx *lexer) errorf(format string, values ...any) stateFn { + if lx.atEOF { + pos := lx.getPos() + if lx.pos >= 1 && lx.input[lx.pos-1] == '\n' { + pos.Line-- + } + pos.Len = 1 + pos.Start = lx.pos - 1 + lx.items <- item{typ: itemError, pos: pos, err: fmt.Errorf(format, values...)} + return nil + } + lx.items <- item{typ: itemError, pos: lx.getPos(), err: fmt.Errorf(format, values...)} + return nil +} + +func (lx *lexer) errorControlChar(cc rune) stateFn { + return lx.errorPos(lx.pos-1, 1, errLexControl{cc}) +} + +// lexTop consumes elements at the top level of TOML data. +func lexTop(lx *lexer) stateFn { + r := lx.next() + if isWhitespace(r) || isNL(r) { + return lexSkip(lx, lexTop) + } + switch r { + case '#': + lx.push(lexTop) + return lexCommentStart + case '[': + return lexTableStart + case eof: + if lx.pos > lx.start { + return lx.errorf("unexpected EOF") + } + lx.emit(itemEOF) + return nil + } + + // At this point, the only valid item can be a key, so we back up + // and let the key lexer do the rest. + lx.backup() + lx.push(lexTopEnd) + return lexKeyStart +} + +// lexTopEnd is entered whenever a top-level item has been consumed. (A value +// or a table.) It must see only whitespace, and will turn back to lexTop +// upon a newline. If it sees EOF, it will quit the lexer successfully. +func lexTopEnd(lx *lexer) stateFn { + r := lx.next() + switch { + case r == '#': + // a comment will read to a newline for us. + lx.push(lexTop) + return lexCommentStart + case isWhitespace(r): + return lexTopEnd + case isNL(r): + lx.ignore() + return lexTop + case r == eof: + lx.emit(itemEOF) + return nil + } + return lx.errorf("expected a top-level item to end with a newline, comment, or EOF, but got %q instead", r) +} + +// lexTable lexes the beginning of a table. Namely, it makes sure that +// it starts with a character other than '.' and ']'. +// It assumes that '[' has already been consumed. +// It also handles the case that this is an item in an array of tables. +// e.g., '[[name]]'. +func lexTableStart(lx *lexer) stateFn { + if lx.peek() == '[' { + lx.next() + lx.emit(itemArrayTableStart) + lx.push(lexArrayTableEnd) + } else { + lx.emit(itemTableStart) + lx.push(lexTableEnd) + } + return lexTableNameStart +} + +func lexTableEnd(lx *lexer) stateFn { + lx.emit(itemTableEnd) + return lexTopEnd +} + +func lexArrayTableEnd(lx *lexer) stateFn { + if r := lx.next(); r != ']' { + return lx.errorf("expected end of table array name delimiter ']', but got %q instead", r) + } + lx.emit(itemArrayTableEnd) + return lexTopEnd +} + +func lexTableNameStart(lx *lexer) stateFn { + lx.skip(isWhitespace) + switch r := lx.peek(); { + case r == ']' || r == eof: + return lx.errorf("unexpected end of table name (table names cannot be empty)") + case r == '.': + return lx.errorf("unexpected table separator (table names cannot be empty)") + case r == '"' || r == '\'': + lx.ignore() + lx.push(lexTableNameEnd) + return lexQuotedName + default: + lx.push(lexTableNameEnd) + return lexBareName + } +} + +// lexTableNameEnd reads the end of a piece of a table name, optionally +// consuming whitespace. +func lexTableNameEnd(lx *lexer) stateFn { + lx.skip(isWhitespace) + switch r := lx.next(); { + case isWhitespace(r): + return lexTableNameEnd + case r == '.': + lx.ignore() + return lexTableNameStart + case r == ']': + return lx.pop() + default: + return lx.errorf("expected '.' or ']' to end table name, but got %q instead", r) + } +} + +// lexBareName lexes one part of a key or table. +// +// It assumes that at least one valid character for the table has already been +// read. +// +// Lexes only one part, e.g. only 'a' inside 'a.b'. +func lexBareName(lx *lexer) stateFn { + r := lx.next() + if isBareKeyChar(r, lx.tomlNext) { + return lexBareName + } + lx.backup() + lx.emit(itemText) + return lx.pop() +} + +// lexBareName lexes one part of a key or table. +// +// It assumes that at least one valid character for the table has already been +// read. +// +// Lexes only one part, e.g. only '"a"' inside '"a".b'. +func lexQuotedName(lx *lexer) stateFn { + r := lx.next() + switch { + case isWhitespace(r): + return lexSkip(lx, lexValue) + case r == '"': + lx.ignore() // ignore the '"' + return lexString + case r == '\'': + lx.ignore() // ignore the "'" + return lexRawString + case r == eof: + return lx.errorf("unexpected EOF; expected value") + default: + return lx.errorf("expected value but found %q instead", r) + } +} + +// lexKeyStart consumes all key parts until a '='. +func lexKeyStart(lx *lexer) stateFn { + lx.skip(isWhitespace) + switch r := lx.peek(); { + case r == '=' || r == eof: + return lx.errorf("unexpected '=': key name appears blank") + case r == '.': + return lx.errorf("unexpected '.': keys cannot start with a '.'") + case r == '"' || r == '\'': + lx.ignore() + fallthrough + default: // Bare key + lx.emit(itemKeyStart) + return lexKeyNameStart + } +} + +func lexKeyNameStart(lx *lexer) stateFn { + lx.skip(isWhitespace) + switch r := lx.peek(); { + case r == '=' || r == eof: + return lx.errorf("unexpected '='") + case r == '.': + return lx.errorf("unexpected '.'") + case r == '"' || r == '\'': + lx.ignore() + lx.push(lexKeyEnd) + return lexQuotedName + default: + lx.push(lexKeyEnd) + return lexBareName + } +} + +// lexKeyEnd consumes the end of a key and trims whitespace (up to the key +// separator). +func lexKeyEnd(lx *lexer) stateFn { + lx.skip(isWhitespace) + switch r := lx.next(); { + case isWhitespace(r): + return lexSkip(lx, lexKeyEnd) + case r == eof: + return lx.errorf("unexpected EOF; expected key separator '='") + case r == '.': + lx.ignore() + return lexKeyNameStart + case r == '=': + lx.emit(itemKeyEnd) + return lexSkip(lx, lexValue) + default: + if r == '\n' { + return lx.errorPrevLine(fmt.Errorf("expected '.' or '=', but got %q instead", r)) + } + return lx.errorf("expected '.' or '=', but got %q instead", r) + } +} + +// lexValue starts the consumption of a value anywhere a value is expected. +// lexValue will ignore whitespace. +// After a value is lexed, the last state on the next is popped and returned. +func lexValue(lx *lexer) stateFn { + // We allow whitespace to precede a value, but NOT newlines. + // In array syntax, the array states are responsible for ignoring newlines. + r := lx.next() + switch { + case isWhitespace(r): + return lexSkip(lx, lexValue) + case isDigit(r): + lx.backup() // avoid an extra state and use the same as above + return lexNumberOrDateStart + } + switch r { + case '[': + lx.ignore() + lx.emit(itemArray) + return lexArrayValue + case '{': + lx.ignore() + lx.emit(itemInlineTableStart) + return lexInlineTableValue + case '"': + if lx.accept('"') { + if lx.accept('"') { + lx.ignore() // Ignore """ + return lexMultilineString + } + lx.backup() + } + lx.ignore() // ignore the '"' + return lexString + case '\'': + if lx.accept('\'') { + if lx.accept('\'') { + lx.ignore() // Ignore """ + return lexMultilineRawString + } + lx.backup() + } + lx.ignore() // ignore the "'" + return lexRawString + case '.': // special error case, be kind to users + return lx.errorf("floats must start with a digit, not '.'") + case 'i', 'n': + if (lx.accept('n') && lx.accept('f')) || (lx.accept('a') && lx.accept('n')) { + lx.emit(itemFloat) + return lx.pop() + } + case '-', '+': + return lexDecimalNumberStart + } + if unicode.IsLetter(r) { + // Be permissive here; lexBool will give a nice error if the + // user wrote something like + // x = foo + // (i.e. not 'true' or 'false' but is something else word-like.) + lx.backup() + return lexBool + } + if r == eof { + return lx.errorf("unexpected EOF; expected value") + } + if r == '\n' { + return lx.errorPrevLine(fmt.Errorf("expected value but found %q instead", r)) + } + return lx.errorf("expected value but found %q instead", r) +} + +// lexArrayValue consumes one value in an array. It assumes that '[' or ',' +// have already been consumed. All whitespace and newlines are ignored. +func lexArrayValue(lx *lexer) stateFn { + r := lx.next() + switch { + case isWhitespace(r) || isNL(r): + return lexSkip(lx, lexArrayValue) + case r == '#': + lx.push(lexArrayValue) + return lexCommentStart + case r == ',': + return lx.errorf("unexpected comma") + case r == ']': + return lexArrayEnd + } + + lx.backup() + lx.push(lexArrayValueEnd) + return lexValue +} + +// lexArrayValueEnd consumes everything between the end of an array value and +// the next value (or the end of the array): it ignores whitespace and newlines +// and expects either a ',' or a ']'. +func lexArrayValueEnd(lx *lexer) stateFn { + switch r := lx.next(); { + case isWhitespace(r) || isNL(r): + return lexSkip(lx, lexArrayValueEnd) + case r == '#': + lx.push(lexArrayValueEnd) + return lexCommentStart + case r == ',': + lx.ignore() + return lexArrayValue // move on to the next value + case r == ']': + return lexArrayEnd + default: + return lx.errorf("expected a comma (',') or array terminator (']'), but got %s", runeOrEOF(r)) + } +} + +// lexArrayEnd finishes the lexing of an array. +// It assumes that a ']' has just been consumed. +func lexArrayEnd(lx *lexer) stateFn { + lx.ignore() + lx.emit(itemArrayEnd) + return lx.pop() +} + +// lexInlineTableValue consumes one key/value pair in an inline table. +// It assumes that '{' or ',' have already been consumed. Whitespace is ignored. +func lexInlineTableValue(lx *lexer) stateFn { + r := lx.next() + switch { + case isWhitespace(r): + return lexSkip(lx, lexInlineTableValue) + case isNL(r): + if lx.tomlNext { + return lexSkip(lx, lexInlineTableValue) + } + return lx.errorPrevLine(errLexInlineTableNL{}) + case r == '#': + lx.push(lexInlineTableValue) + return lexCommentStart + case r == ',': + return lx.errorf("unexpected comma") + case r == '}': + return lexInlineTableEnd + } + lx.backup() + lx.push(lexInlineTableValueEnd) + return lexKeyStart +} + +// lexInlineTableValueEnd consumes everything between the end of an inline table +// key/value pair and the next pair (or the end of the table): +// it ignores whitespace and expects either a ',' or a '}'. +func lexInlineTableValueEnd(lx *lexer) stateFn { + switch r := lx.next(); { + case isWhitespace(r): + return lexSkip(lx, lexInlineTableValueEnd) + case isNL(r): + if lx.tomlNext { + return lexSkip(lx, lexInlineTableValueEnd) + } + return lx.errorPrevLine(errLexInlineTableNL{}) + case r == '#': + lx.push(lexInlineTableValueEnd) + return lexCommentStart + case r == ',': + lx.ignore() + lx.skip(isWhitespace) + if lx.peek() == '}' { + if lx.tomlNext { + return lexInlineTableValueEnd + } + return lx.errorf("trailing comma not allowed in inline tables") + } + return lexInlineTableValue + case r == '}': + return lexInlineTableEnd + default: + return lx.errorf("expected a comma or an inline table terminator '}', but got %s instead", runeOrEOF(r)) + } +} + +func runeOrEOF(r rune) string { + if r == eof { + return "end of file" + } + return "'" + string(r) + "'" +} + +// lexInlineTableEnd finishes the lexing of an inline table. +// It assumes that a '}' has just been consumed. +func lexInlineTableEnd(lx *lexer) stateFn { + lx.ignore() + lx.emit(itemInlineTableEnd) + return lx.pop() +} + +// lexString consumes the inner contents of a string. It assumes that the +// beginning '"' has already been consumed and ignored. +func lexString(lx *lexer) stateFn { + r := lx.next() + switch { + case r == eof: + return lx.errorf(`unexpected EOF; expected '"'`) + case isNL(r): + return lx.errorPrevLine(errLexStringNL{}) + case r == '\\': + lx.push(lexString) + return lexStringEscape + case r == '"': + lx.backup() + if lx.esc { + lx.esc = false + lx.emit(itemStringEsc) + } else { + lx.emit(itemString) + } + lx.next() + lx.ignore() + return lx.pop() + } + return lexString +} + +// lexMultilineString consumes the inner contents of a string. It assumes that +// the beginning '"""' has already been consumed and ignored. +func lexMultilineString(lx *lexer) stateFn { + r := lx.next() + switch r { + default: + return lexMultilineString + case eof: + return lx.errorf(`unexpected EOF; expected '"""'`) + case '\\': + return lexMultilineStringEscape + case '"': + /// Found " → try to read two more "". + if lx.accept('"') { + if lx.accept('"') { + /// Peek ahead: the string can contain " and "", including at the + /// end: """str""""" + /// 6 or more at the end, however, is an error. + if lx.peek() == '"' { + /// Check if we already lexed 5 's; if so we have 6 now, and + /// that's just too many man! + /// + /// Second check is for the edge case: + /// + /// two quotes allowed. + /// vv + /// """lol \"""""" + /// ^^ ^^^---- closing three + /// escaped + /// + /// But ugly, but it works + if strings.HasSuffix(lx.current(), `"""""`) && !strings.HasSuffix(lx.current(), `\"""""`) { + return lx.errorf(`unexpected '""""""'`) + } + lx.backup() + lx.backup() + return lexMultilineString + } + + lx.backup() /// backup: don't include the """ in the item. + lx.backup() + lx.backup() + lx.esc = false + lx.emit(itemMultilineString) + lx.next() /// Read over ''' again and discard it. + lx.next() + lx.next() + lx.ignore() + return lx.pop() + } + lx.backup() + } + return lexMultilineString + } +} + +// lexRawString consumes a raw string. Nothing can be escaped in such a string. +// It assumes that the beginning "'" has already been consumed and ignored. +func lexRawString(lx *lexer) stateFn { + r := lx.next() + switch { + default: + return lexRawString + case r == eof: + return lx.errorf(`unexpected EOF; expected "'"`) + case isNL(r): + return lx.errorPrevLine(errLexStringNL{}) + case r == '\'': + lx.backup() + lx.emit(itemRawString) + lx.next() + lx.ignore() + return lx.pop() + } +} + +// lexMultilineRawString consumes a raw string. Nothing can be escaped in such a +// string. It assumes that the beginning triple-' has already been consumed and +// ignored. +func lexMultilineRawString(lx *lexer) stateFn { + r := lx.next() + switch r { + default: + return lexMultilineRawString + case eof: + return lx.errorf(`unexpected EOF; expected "'''"`) + case '\'': + /// Found ' → try to read two more ''. + if lx.accept('\'') { + if lx.accept('\'') { + /// Peek ahead: the string can contain ' and '', including at the + /// end: '''str''''' + /// 6 or more at the end, however, is an error. + if lx.peek() == '\'' { + /// Check if we already lexed 5 's; if so we have 6 now, and + /// that's just too many man! + if strings.HasSuffix(lx.current(), "'''''") { + return lx.errorf(`unexpected "''''''"`) + } + lx.backup() + lx.backup() + return lexMultilineRawString + } + + lx.backup() /// backup: don't include the ''' in the item. + lx.backup() + lx.backup() + lx.emit(itemRawMultilineString) + lx.next() /// Read over ''' again and discard it. + lx.next() + lx.next() + lx.ignore() + return lx.pop() + } + lx.backup() + } + return lexMultilineRawString + } +} + +// lexMultilineStringEscape consumes an escaped character. It assumes that the +// preceding '\\' has already been consumed. +func lexMultilineStringEscape(lx *lexer) stateFn { + if isNL(lx.next()) { /// \ escaping newline. + return lexMultilineString + } + lx.backup() + lx.push(lexMultilineString) + return lexStringEscape(lx) +} + +func lexStringEscape(lx *lexer) stateFn { + lx.esc = true + r := lx.next() + switch r { + case 'e': + if !lx.tomlNext { + return lx.error(errLexEscape{r}) + } + fallthrough + case 'b': + fallthrough + case 't': + fallthrough + case 'n': + fallthrough + case 'f': + fallthrough + case 'r': + fallthrough + case '"': + fallthrough + case ' ', '\t': + // Inside """ .. """ strings you can use \ to escape newlines, and any + // amount of whitespace can be between the \ and \n. + fallthrough + case '\\': + return lx.pop() + case 'x': + if !lx.tomlNext { + return lx.error(errLexEscape{r}) + } + return lexHexEscape + case 'u': + return lexShortUnicodeEscape + case 'U': + return lexLongUnicodeEscape + } + return lx.error(errLexEscape{r}) +} + +func lexHexEscape(lx *lexer) stateFn { + var r rune + for i := 0; i < 2; i++ { + r = lx.next() + if !isHex(r) { + return lx.errorf(`expected two hexadecimal digits after '\x', but got %q instead`, lx.current()) + } + } + return lx.pop() +} + +func lexShortUnicodeEscape(lx *lexer) stateFn { + var r rune + for i := 0; i < 4; i++ { + r = lx.next() + if !isHex(r) { + return lx.errorf(`expected four hexadecimal digits after '\u', but got %q instead`, lx.current()) + } + } + return lx.pop() +} + +func lexLongUnicodeEscape(lx *lexer) stateFn { + var r rune + for i := 0; i < 8; i++ { + r = lx.next() + if !isHex(r) { + return lx.errorf(`expected eight hexadecimal digits after '\U', but got %q instead`, lx.current()) + } + } + return lx.pop() +} + +// lexNumberOrDateStart processes the first character of a value which begins +// with a digit. It exists to catch values starting with '0', so that +// lexBaseNumberOrDate can differentiate base prefixed integers from other +// types. +func lexNumberOrDateStart(lx *lexer) stateFn { + r := lx.next() + switch r { + case '0': + return lexBaseNumberOrDate + } + + if !isDigit(r) { + // The only way to reach this state is if the value starts + // with a digit, so specifically treat anything else as an + // error. + return lx.errorf("expected a digit but got %q", r) + } + + return lexNumberOrDate +} + +// lexNumberOrDate consumes either an integer, float or datetime. +func lexNumberOrDate(lx *lexer) stateFn { + r := lx.next() + if isDigit(r) { + return lexNumberOrDate + } + switch r { + case '-', ':': + return lexDatetime + case '_': + return lexDecimalNumber + case '.', 'e', 'E': + return lexFloat + } + + lx.backup() + lx.emit(itemInteger) + return lx.pop() +} + +// lexDatetime consumes a Datetime, to a first approximation. +// The parser validates that it matches one of the accepted formats. +func lexDatetime(lx *lexer) stateFn { + r := lx.next() + if isDigit(r) { + return lexDatetime + } + switch r { + case '-', ':', 'T', 't', ' ', '.', 'Z', 'z', '+': + return lexDatetime + } + + lx.backup() + lx.emitTrim(itemDatetime) + return lx.pop() +} + +// lexHexInteger consumes a hexadecimal integer after seeing the '0x' prefix. +func lexHexInteger(lx *lexer) stateFn { + r := lx.next() + if isHex(r) { + return lexHexInteger + } + switch r { + case '_': + return lexHexInteger + } + + lx.backup() + lx.emit(itemInteger) + return lx.pop() +} + +// lexOctalInteger consumes an octal integer after seeing the '0o' prefix. +func lexOctalInteger(lx *lexer) stateFn { + r := lx.next() + if isOctal(r) { + return lexOctalInteger + } + switch r { + case '_': + return lexOctalInteger + } + + lx.backup() + lx.emit(itemInteger) + return lx.pop() +} + +// lexBinaryInteger consumes a binary integer after seeing the '0b' prefix. +func lexBinaryInteger(lx *lexer) stateFn { + r := lx.next() + if isBinary(r) { + return lexBinaryInteger + } + switch r { + case '_': + return lexBinaryInteger + } + + lx.backup() + lx.emit(itemInteger) + return lx.pop() +} + +// lexDecimalNumber consumes a decimal float or integer. +func lexDecimalNumber(lx *lexer) stateFn { + r := lx.next() + if isDigit(r) { + return lexDecimalNumber + } + switch r { + case '.', 'e', 'E': + return lexFloat + case '_': + return lexDecimalNumber + } + + lx.backup() + lx.emit(itemInteger) + return lx.pop() +} + +// lexDecimalNumber consumes the first digit of a number beginning with a sign. +// It assumes the sign has already been consumed. Values which start with a sign +// are only allowed to be decimal integers or floats. +// +// The special "nan" and "inf" values are also recognized. +func lexDecimalNumberStart(lx *lexer) stateFn { + r := lx.next() + + // Special error cases to give users better error messages + switch r { + case 'i': + if !lx.accept('n') || !lx.accept('f') { + return lx.errorf("invalid float: '%s'", lx.current()) + } + lx.emit(itemFloat) + return lx.pop() + case 'n': + if !lx.accept('a') || !lx.accept('n') { + return lx.errorf("invalid float: '%s'", lx.current()) + } + lx.emit(itemFloat) + return lx.pop() + case '0': + p := lx.peek() + switch p { + case 'b', 'o', 'x': + return lx.errorf("cannot use sign with non-decimal numbers: '%s%c'", lx.current(), p) + } + case '.': + return lx.errorf("floats must start with a digit, not '.'") + } + + if isDigit(r) { + return lexDecimalNumber + } + + return lx.errorf("expected a digit but got %q", r) +} + +// lexBaseNumberOrDate differentiates between the possible values which +// start with '0'. It assumes that before reaching this state, the initial '0' +// has been consumed. +func lexBaseNumberOrDate(lx *lexer) stateFn { + r := lx.next() + // Note: All datetimes start with at least two digits, so we don't + // handle date characters (':', '-', etc.) here. + if isDigit(r) { + return lexNumberOrDate + } + switch r { + case '_': + // Can only be decimal, because there can't be an underscore + // between the '0' and the base designator, and dates can't + // contain underscores. + return lexDecimalNumber + case '.', 'e', 'E': + return lexFloat + case 'b': + r = lx.peek() + if !isBinary(r) { + lx.errorf("not a binary number: '%s%c'", lx.current(), r) + } + return lexBinaryInteger + case 'o': + r = lx.peek() + if !isOctal(r) { + lx.errorf("not an octal number: '%s%c'", lx.current(), r) + } + return lexOctalInteger + case 'x': + r = lx.peek() + if !isHex(r) { + lx.errorf("not a hexadecimal number: '%s%c'", lx.current(), r) + } + return lexHexInteger + } + + lx.backup() + lx.emit(itemInteger) + return lx.pop() +} + +// lexFloat consumes the elements of a float. It allows any sequence of +// float-like characters, so floats emitted by the lexer are only a first +// approximation and must be validated by the parser. +func lexFloat(lx *lexer) stateFn { + r := lx.next() + if isDigit(r) { + return lexFloat + } + switch r { + case '_', '.', '-', '+', 'e', 'E': + return lexFloat + } + + lx.backup() + lx.emit(itemFloat) + return lx.pop() +} + +// lexBool consumes a bool string: 'true' or 'false. +func lexBool(lx *lexer) stateFn { + var rs []rune + for { + r := lx.next() + if !unicode.IsLetter(r) { + lx.backup() + break + } + rs = append(rs, r) + } + s := string(rs) + switch s { + case "true", "false": + lx.emit(itemBool) + return lx.pop() + } + return lx.errorf("expected value but found %q instead", s) +} + +// lexCommentStart begins the lexing of a comment. It will emit +// itemCommentStart and consume no characters, passing control to lexComment. +func lexCommentStart(lx *lexer) stateFn { + lx.ignore() + lx.emit(itemCommentStart) + return lexComment +} + +// lexComment lexes an entire comment. It assumes that '#' has been consumed. +// It will consume *up to* the first newline character, and pass control +// back to the last state on the stack. +func lexComment(lx *lexer) stateFn { + switch r := lx.next(); { + case isNL(r) || r == eof: + lx.backup() + lx.emit(itemText) + return lx.pop() + default: + return lexComment + } +} + +// lexSkip ignores all slurped input and moves on to the next state. +func lexSkip(lx *lexer, nextState stateFn) stateFn { + lx.ignore() + return nextState +} + +func (s stateFn) String() string { + name := runtime.FuncForPC(reflect.ValueOf(s).Pointer()).Name() + if i := strings.LastIndexByte(name, '.'); i > -1 { + name = name[i+1:] + } + if s == nil { + name = "" + } + return name + "()" +} + +func (itype itemType) String() string { + switch itype { + case itemError: + return "Error" + case itemNIL: + return "NIL" + case itemEOF: + return "EOF" + case itemText: + return "Text" + case itemString, itemStringEsc, itemRawString, itemMultilineString, itemRawMultilineString: + return "String" + case itemBool: + return "Bool" + case itemInteger: + return "Integer" + case itemFloat: + return "Float" + case itemDatetime: + return "DateTime" + case itemTableStart: + return "TableStart" + case itemTableEnd: + return "TableEnd" + case itemKeyStart: + return "KeyStart" + case itemKeyEnd: + return "KeyEnd" + case itemArray: + return "Array" + case itemArrayEnd: + return "ArrayEnd" + case itemCommentStart: + return "CommentStart" + case itemInlineTableStart: + return "InlineTableStart" + case itemInlineTableEnd: + return "InlineTableEnd" + } + panic(fmt.Sprintf("BUG: Unknown type '%d'.", int(itype))) +} + +func (item item) String() string { + return fmt.Sprintf("(%s, %s)", item.typ, item.val) +} + +func isWhitespace(r rune) bool { return r == '\t' || r == ' ' } +func isNL(r rune) bool { return r == '\n' || r == '\r' } +func isControl(r rune) bool { // Control characters except \t, \r, \n + switch r { + case '\t', '\r', '\n': + return false + default: + return (r >= 0x00 && r <= 0x1f) || r == 0x7f + } +} +func isDigit(r rune) bool { return r >= '0' && r <= '9' } +func isBinary(r rune) bool { return r == '0' || r == '1' } +func isOctal(r rune) bool { return r >= '0' && r <= '7' } +func isHex(r rune) bool { return (r >= '0' && r <= '9') || (r|0x20 >= 'a' && r|0x20 <= 'f') } +func isBareKeyChar(r rune, tomlNext bool) bool { + return (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') || + (r >= '0' && r <= '9') || r == '_' || r == '-' +} diff --git a/vendor/github.com/BurntSushi/toml/meta.go b/vendor/github.com/BurntSushi/toml/meta.go new file mode 100644 index 0000000..0d33702 --- /dev/null +++ b/vendor/github.com/BurntSushi/toml/meta.go @@ -0,0 +1,145 @@ +package toml + +import ( + "strings" +) + +// MetaData allows access to meta information about TOML data that's not +// accessible otherwise. +// +// It allows checking if a key is defined in the TOML data, whether any keys +// were undecoded, and the TOML type of a key. +type MetaData struct { + context Key // Used only during decoding. + + keyInfo map[string]keyInfo + mapping map[string]any + keys []Key + decoded map[string]struct{} + data []byte // Input file; for errors. +} + +// IsDefined reports if the key exists in the TOML data. +// +// The key should be specified hierarchically, for example to access the TOML +// key "a.b.c" you would use IsDefined("a", "b", "c"). Keys are case sensitive. +// +// Returns false for an empty key. +func (md *MetaData) IsDefined(key ...string) bool { + if len(key) == 0 { + return false + } + + var ( + hash map[string]any + ok bool + hashOrVal any = md.mapping + ) + for _, k := range key { + if hash, ok = hashOrVal.(map[string]any); !ok { + return false + } + if hashOrVal, ok = hash[k]; !ok { + return false + } + } + return true +} + +// Type returns a string representation of the type of the key specified. +// +// Type will return the empty string if given an empty key or a key that does +// not exist. Keys are case sensitive. +func (md *MetaData) Type(key ...string) string { + if ki, ok := md.keyInfo[Key(key).String()]; ok { + return ki.tomlType.typeString() + } + return "" +} + +// Keys returns a slice of every key in the TOML data, including key groups. +// +// Each key is itself a slice, where the first element is the top of the +// hierarchy and the last is the most specific. The list will have the same +// order as the keys appeared in the TOML data. +// +// All keys returned are non-empty. +func (md *MetaData) Keys() []Key { + return md.keys +} + +// Undecoded returns all keys that have not been decoded in the order in which +// they appear in the original TOML document. +// +// This includes keys that haven't been decoded because of a [Primitive] value. +// Once the Primitive value is decoded, the keys will be considered decoded. +// +// Also note that decoding into an empty interface will result in no decoding, +// and so no keys will be considered decoded. +// +// In this sense, the Undecoded keys correspond to keys in the TOML document +// that do not have a concrete type in your representation. +func (md *MetaData) Undecoded() []Key { + undecoded := make([]Key, 0, len(md.keys)) + for _, key := range md.keys { + if _, ok := md.decoded[key.String()]; !ok { + undecoded = append(undecoded, key) + } + } + return undecoded +} + +// Key represents any TOML key, including key groups. Use [MetaData.Keys] to get +// values of this type. +type Key []string + +func (k Key) String() string { + // This is called quite often, so it's a bit funky to make it faster. + var b strings.Builder + b.Grow(len(k) * 25) +outer: + for i, kk := range k { + if i > 0 { + b.WriteByte('.') + } + if kk == "" { + b.WriteString(`""`) + } else { + for _, r := range kk { + // "Inline" isBareKeyChar + if !((r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '_' || r == '-') { + b.WriteByte('"') + b.WriteString(dblQuotedReplacer.Replace(kk)) + b.WriteByte('"') + continue outer + } + } + b.WriteString(kk) + } + } + return b.String() +} + +func (k Key) maybeQuoted(i int) string { + if k[i] == "" { + return `""` + } + for _, r := range k[i] { + if (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '_' || r == '-' { + continue + } + return `"` + dblQuotedReplacer.Replace(k[i]) + `"` + } + return k[i] +} + +// Like append(), but only increase the cap by 1. +func (k Key) add(piece string) Key { + newKey := make(Key, len(k)+1) + copy(newKey, k) + newKey[len(k)] = piece + return newKey +} + +func (k Key) parent() Key { return k[:len(k)-1] } // all except the last piece. +func (k Key) last() string { return k[len(k)-1] } // last piece of this key. diff --git a/vendor/github.com/BurntSushi/toml/parse.go b/vendor/github.com/BurntSushi/toml/parse.go new file mode 100644 index 0000000..e3ea8a9 --- /dev/null +++ b/vendor/github.com/BurntSushi/toml/parse.go @@ -0,0 +1,845 @@ +package toml + +import ( + "fmt" + "math" + "os" + "strconv" + "strings" + "time" + "unicode/utf8" + + "github.com/BurntSushi/toml/internal" +) + +type parser struct { + lx *lexer + context Key // Full key for the current hash in scope. + currentKey string // Base key name for everything except hashes. + pos Position // Current position in the TOML file. + tomlNext bool + + ordered []Key // List of keys in the order that they appear in the TOML data. + + keyInfo map[string]keyInfo // Map keyname → info about the TOML key. + mapping map[string]any // Map keyname → key value. + implicits map[string]struct{} // Record implicit keys (e.g. "key.group.names"). +} + +type keyInfo struct { + pos Position + tomlType tomlType +} + +func parse(data string) (p *parser, err error) { + _, tomlNext := os.LookupEnv("BURNTSUSHI_TOML_110") + + defer func() { + if r := recover(); r != nil { + if pErr, ok := r.(ParseError); ok { + pErr.input = data + err = pErr + return + } + panic(r) + } + }() + + // Read over BOM; do this here as the lexer calls utf8.DecodeRuneInString() + // which mangles stuff. UTF-16 BOM isn't strictly valid, but some tools add + // it anyway. + if strings.HasPrefix(data, "\xff\xfe") || strings.HasPrefix(data, "\xfe\xff") { // UTF-16 + data = data[2:] + } else if strings.HasPrefix(data, "\xef\xbb\xbf") { // UTF-8 + data = data[3:] + } + + // Examine first few bytes for NULL bytes; this probably means it's a UTF-16 + // file (second byte in surrogate pair being NULL). Again, do this here to + // avoid having to deal with UTF-8/16 stuff in the lexer. + ex := 6 + if len(data) < 6 { + ex = len(data) + } + if i := strings.IndexRune(data[:ex], 0); i > -1 { + return nil, ParseError{ + Message: "files cannot contain NULL bytes; probably using UTF-16; TOML files must be UTF-8", + Position: Position{Line: 1, Col: 1, Start: i, Len: 1}, + Line: 1, + input: data, + } + } + + p = &parser{ + keyInfo: make(map[string]keyInfo), + mapping: make(map[string]any), + lx: lex(data, tomlNext), + ordered: make([]Key, 0), + implicits: make(map[string]struct{}), + tomlNext: tomlNext, + } + for { + item := p.next() + if item.typ == itemEOF { + break + } + p.topLevel(item) + } + + return p, nil +} + +func (p *parser) panicErr(it item, err error) { + panic(ParseError{ + Message: err.Error(), + err: err, + Position: it.pos.withCol(p.lx.input), + Line: it.pos.Len, + LastKey: p.current(), + }) +} + +func (p *parser) panicItemf(it item, format string, v ...any) { + panic(ParseError{ + Message: fmt.Sprintf(format, v...), + Position: it.pos.withCol(p.lx.input), + Line: it.pos.Len, + LastKey: p.current(), + }) +} + +func (p *parser) panicf(format string, v ...any) { + panic(ParseError{ + Message: fmt.Sprintf(format, v...), + Position: p.pos.withCol(p.lx.input), + Line: p.pos.Line, + LastKey: p.current(), + }) +} + +func (p *parser) next() item { + it := p.lx.nextItem() + //fmt.Printf("ITEM %-18s line %-3d │ %q\n", it.typ, it.pos.Line, it.val) + if it.typ == itemError { + if it.err != nil { + panic(ParseError{ + Message: it.err.Error(), + err: it.err, + Position: it.pos.withCol(p.lx.input), + Line: it.pos.Line, + LastKey: p.current(), + }) + } + + p.panicItemf(it, "%s", it.val) + } + return it +} + +func (p *parser) nextPos() item { + it := p.next() + p.pos = it.pos + return it +} + +func (p *parser) bug(format string, v ...any) { + panic(fmt.Sprintf("BUG: "+format+"\n\n", v...)) +} + +func (p *parser) expect(typ itemType) item { + it := p.next() + p.assertEqual(typ, it.typ) + return it +} + +func (p *parser) assertEqual(expected, got itemType) { + if expected != got { + p.bug("Expected '%s' but got '%s'.", expected, got) + } +} + +func (p *parser) topLevel(item item) { + switch item.typ { + case itemCommentStart: // # .. + p.expect(itemText) + case itemTableStart: // [ .. ] + name := p.nextPos() + + var key Key + for ; name.typ != itemTableEnd && name.typ != itemEOF; name = p.next() { + key = append(key, p.keyString(name)) + } + p.assertEqual(itemTableEnd, name.typ) + + p.addContext(key, false) + p.setType("", tomlHash, item.pos) + p.ordered = append(p.ordered, key) + case itemArrayTableStart: // [[ .. ]] + name := p.nextPos() + + var key Key + for ; name.typ != itemArrayTableEnd && name.typ != itemEOF; name = p.next() { + key = append(key, p.keyString(name)) + } + p.assertEqual(itemArrayTableEnd, name.typ) + + p.addContext(key, true) + p.setType("", tomlArrayHash, item.pos) + p.ordered = append(p.ordered, key) + case itemKeyStart: // key = .. + outerContext := p.context + /// Read all the key parts (e.g. 'a' and 'b' in 'a.b') + k := p.nextPos() + var key Key + for ; k.typ != itemKeyEnd && k.typ != itemEOF; k = p.next() { + key = append(key, p.keyString(k)) + } + p.assertEqual(itemKeyEnd, k.typ) + + /// The current key is the last part. + p.currentKey = key.last() + + /// All the other parts (if any) are the context; need to set each part + /// as implicit. + context := key.parent() + for i := range context { + p.addImplicitContext(append(p.context, context[i:i+1]...)) + } + p.ordered = append(p.ordered, p.context.add(p.currentKey)) + + /// Set value. + vItem := p.next() + val, typ := p.value(vItem, false) + p.setValue(p.currentKey, val) + p.setType(p.currentKey, typ, vItem.pos) + + /// Remove the context we added (preserving any context from [tbl] lines). + p.context = outerContext + p.currentKey = "" + default: + p.bug("Unexpected type at top level: %s", item.typ) + } +} + +// Gets a string for a key (or part of a key in a table name). +func (p *parser) keyString(it item) string { + switch it.typ { + case itemText: + return it.val + case itemString, itemStringEsc, itemMultilineString, + itemRawString, itemRawMultilineString: + s, _ := p.value(it, false) + return s.(string) + default: + p.bug("Unexpected key type: %s", it.typ) + } + panic("unreachable") +} + +var datetimeRepl = strings.NewReplacer( + "z", "Z", + "t", "T", + " ", "T") + +// value translates an expected value from the lexer into a Go value wrapped +// as an empty interface. +func (p *parser) value(it item, parentIsArray bool) (any, tomlType) { + switch it.typ { + case itemString: + return it.val, p.typeOfPrimitive(it) + case itemStringEsc: + return p.replaceEscapes(it, it.val), p.typeOfPrimitive(it) + case itemMultilineString: + return p.replaceEscapes(it, p.stripEscapedNewlines(stripFirstNewline(it.val))), p.typeOfPrimitive(it) + case itemRawString: + return it.val, p.typeOfPrimitive(it) + case itemRawMultilineString: + return stripFirstNewline(it.val), p.typeOfPrimitive(it) + case itemInteger: + return p.valueInteger(it) + case itemFloat: + return p.valueFloat(it) + case itemBool: + switch it.val { + case "true": + return true, p.typeOfPrimitive(it) + case "false": + return false, p.typeOfPrimitive(it) + default: + p.bug("Expected boolean value, but got '%s'.", it.val) + } + case itemDatetime: + return p.valueDatetime(it) + case itemArray: + return p.valueArray(it) + case itemInlineTableStart: + return p.valueInlineTable(it, parentIsArray) + default: + p.bug("Unexpected value type: %s", it.typ) + } + panic("unreachable") +} + +func (p *parser) valueInteger(it item) (any, tomlType) { + if !numUnderscoresOK(it.val) { + p.panicItemf(it, "Invalid integer %q: underscores must be surrounded by digits", it.val) + } + if numHasLeadingZero(it.val) { + p.panicItemf(it, "Invalid integer %q: cannot have leading zeroes", it.val) + } + + num, err := strconv.ParseInt(it.val, 0, 64) + if err != nil { + // Distinguish integer values. Normally, it'd be a bug if the lexer + // provides an invalid integer, but it's possible that the number is + // out of range of valid values (which the lexer cannot determine). + // So mark the former as a bug but the latter as a legitimate user + // error. + if e, ok := err.(*strconv.NumError); ok && e.Err == strconv.ErrRange { + p.panicErr(it, errParseRange{i: it.val, size: "int64"}) + } else { + p.bug("Expected integer value, but got '%s'.", it.val) + } + } + return num, p.typeOfPrimitive(it) +} + +func (p *parser) valueFloat(it item) (any, tomlType) { + parts := strings.FieldsFunc(it.val, func(r rune) bool { + switch r { + case '.', 'e', 'E': + return true + } + return false + }) + for _, part := range parts { + if !numUnderscoresOK(part) { + p.panicItemf(it, "Invalid float %q: underscores must be surrounded by digits", it.val) + } + } + if len(parts) > 0 && numHasLeadingZero(parts[0]) { + p.panicItemf(it, "Invalid float %q: cannot have leading zeroes", it.val) + } + if !numPeriodsOK(it.val) { + // As a special case, numbers like '123.' or '1.e2', + // which are valid as far as Go/strconv are concerned, + // must be rejected because TOML says that a fractional + // part consists of '.' followed by 1+ digits. + p.panicItemf(it, "Invalid float %q: '.' must be followed by one or more digits", it.val) + } + val := strings.Replace(it.val, "_", "", -1) + signbit := false + if val == "+nan" || val == "-nan" { + signbit = val == "-nan" + val = "nan" + } + num, err := strconv.ParseFloat(val, 64) + if err != nil { + if e, ok := err.(*strconv.NumError); ok && e.Err == strconv.ErrRange { + p.panicErr(it, errParseRange{i: it.val, size: "float64"}) + } else { + p.panicItemf(it, "Invalid float value: %q", it.val) + } + } + if signbit { + num = math.Copysign(num, -1) + } + return num, p.typeOfPrimitive(it) +} + +var dtTypes = []struct { + fmt string + zone *time.Location + next bool +}{ + {time.RFC3339Nano, time.Local, false}, + {"2006-01-02T15:04:05.999999999", internal.LocalDatetime, false}, + {"2006-01-02", internal.LocalDate, false}, + {"15:04:05.999999999", internal.LocalTime, false}, + + // tomlNext + {"2006-01-02T15:04Z07:00", time.Local, true}, + {"2006-01-02T15:04", internal.LocalDatetime, true}, + {"15:04", internal.LocalTime, true}, +} + +func (p *parser) valueDatetime(it item) (any, tomlType) { + it.val = datetimeRepl.Replace(it.val) + var ( + t time.Time + ok bool + err error + ) + for _, dt := range dtTypes { + if dt.next && !p.tomlNext { + continue + } + t, err = time.ParseInLocation(dt.fmt, it.val, dt.zone) + if err == nil { + if missingLeadingZero(it.val, dt.fmt) { + p.panicErr(it, errParseDate{it.val}) + } + ok = true + break + } + } + if !ok { + p.panicErr(it, errParseDate{it.val}) + } + return t, p.typeOfPrimitive(it) +} + +// Go's time.Parse() will accept numbers without a leading zero; there isn't any +// way to require it. https://github.com/golang/go/issues/29911 +// +// Depend on the fact that the separators (- and :) should always be at the same +// location. +func missingLeadingZero(d, l string) bool { + for i, c := range []byte(l) { + if c == '.' || c == 'Z' { + return false + } + if (c < '0' || c > '9') && d[i] != c { + return true + } + } + return false +} + +func (p *parser) valueArray(it item) (any, tomlType) { + p.setType(p.currentKey, tomlArray, it.pos) + + var ( + // Initialize to a non-nil slice to make it consistent with how S = [] + // decodes into a non-nil slice inside something like struct { S + // []string }. See #338 + array = make([]any, 0, 2) + ) + for it = p.next(); it.typ != itemArrayEnd; it = p.next() { + if it.typ == itemCommentStart { + p.expect(itemText) + continue + } + + val, typ := p.value(it, true) + array = append(array, val) + + // XXX: type isn't used here, we need it to record the accurate type + // information. + // + // Not entirely sure how to best store this; could use "key[0]", + // "key[1]" notation, or maybe store it on the Array type? + _ = typ + } + return array, tomlArray +} + +func (p *parser) valueInlineTable(it item, parentIsArray bool) (any, tomlType) { + var ( + topHash = make(map[string]any) + outerContext = p.context + outerKey = p.currentKey + ) + + p.context = append(p.context, p.currentKey) + prevContext := p.context + p.currentKey = "" + + p.addImplicit(p.context) + p.addContext(p.context, parentIsArray) + + /// Loop over all table key/value pairs. + for it := p.next(); it.typ != itemInlineTableEnd; it = p.next() { + if it.typ == itemCommentStart { + p.expect(itemText) + continue + } + + /// Read all key parts. + k := p.nextPos() + var key Key + for ; k.typ != itemKeyEnd && k.typ != itemEOF; k = p.next() { + key = append(key, p.keyString(k)) + } + p.assertEqual(itemKeyEnd, k.typ) + + /// The current key is the last part. + p.currentKey = key.last() + + /// All the other parts (if any) are the context; need to set each part + /// as implicit. + context := key.parent() + for i := range context { + p.addImplicitContext(append(p.context, context[i:i+1]...)) + } + p.ordered = append(p.ordered, p.context.add(p.currentKey)) + + /// Set the value. + val, typ := p.value(p.next(), false) + p.setValue(p.currentKey, val) + p.setType(p.currentKey, typ, it.pos) + + hash := topHash + for _, c := range context { + h, ok := hash[c] + if !ok { + h = make(map[string]any) + hash[c] = h + } + hash, ok = h.(map[string]any) + if !ok { + p.panicf("%q is not a table", p.context) + } + } + hash[p.currentKey] = val + + /// Restore context. + p.context = prevContext + } + p.context = outerContext + p.currentKey = outerKey + return topHash, tomlHash +} + +// numHasLeadingZero checks if this number has leading zeroes, allowing for '0', +// +/- signs, and base prefixes. +func numHasLeadingZero(s string) bool { + if len(s) > 1 && s[0] == '0' && !(s[1] == 'b' || s[1] == 'o' || s[1] == 'x') { // Allow 0b, 0o, 0x + return true + } + if len(s) > 2 && (s[0] == '-' || s[0] == '+') && s[1] == '0' { + return true + } + return false +} + +// numUnderscoresOK checks whether each underscore in s is surrounded by +// characters that are not underscores. +func numUnderscoresOK(s string) bool { + switch s { + case "nan", "+nan", "-nan", "inf", "-inf", "+inf": + return true + } + accept := false + for _, r := range s { + if r == '_' { + if !accept { + return false + } + } + + // isHex is a superset of all the permissible characters surrounding an + // underscore. + accept = isHex(r) + } + return accept +} + +// numPeriodsOK checks whether every period in s is followed by a digit. +func numPeriodsOK(s string) bool { + period := false + for _, r := range s { + if period && !isDigit(r) { + return false + } + period = r == '.' + } + return !period +} + +// Set the current context of the parser, where the context is either a hash or +// an array of hashes, depending on the value of the `array` parameter. +// +// Establishing the context also makes sure that the key isn't a duplicate, and +// will create implicit hashes automatically. +func (p *parser) addContext(key Key, array bool) { + /// Always start at the top level and drill down for our context. + hashContext := p.mapping + keyContext := make(Key, 0, len(key)-1) + + /// We only need implicit hashes for the parents. + for _, k := range key.parent() { + _, ok := hashContext[k] + keyContext = append(keyContext, k) + + // No key? Make an implicit hash and move on. + if !ok { + p.addImplicit(keyContext) + hashContext[k] = make(map[string]any) + } + + // If the hash context is actually an array of tables, then set + // the hash context to the last element in that array. + // + // Otherwise, it better be a table, since this MUST be a key group (by + // virtue of it not being the last element in a key). + switch t := hashContext[k].(type) { + case []map[string]any: + hashContext = t[len(t)-1] + case map[string]any: + hashContext = t + default: + p.panicf("Key '%s' was already created as a hash.", keyContext) + } + } + + p.context = keyContext + if array { + // If this is the first element for this array, then allocate a new + // list of tables for it. + k := key.last() + if _, ok := hashContext[k]; !ok { + hashContext[k] = make([]map[string]any, 0, 4) + } + + // Add a new table. But make sure the key hasn't already been used + // for something else. + if hash, ok := hashContext[k].([]map[string]any); ok { + hashContext[k] = append(hash, make(map[string]any)) + } else { + p.panicf("Key '%s' was already created and cannot be used as an array.", key) + } + } else { + p.setValue(key.last(), make(map[string]any)) + } + p.context = append(p.context, key.last()) +} + +// setValue sets the given key to the given value in the current context. +// It will make sure that the key hasn't already been defined, account for +// implicit key groups. +func (p *parser) setValue(key string, value any) { + var ( + tmpHash any + ok bool + hash = p.mapping + keyContext = make(Key, 0, len(p.context)+1) + ) + for _, k := range p.context { + keyContext = append(keyContext, k) + if tmpHash, ok = hash[k]; !ok { + p.bug("Context for key '%s' has not been established.", keyContext) + } + switch t := tmpHash.(type) { + case []map[string]any: + // The context is a table of hashes. Pick the most recent table + // defined as the current hash. + hash = t[len(t)-1] + case map[string]any: + hash = t + default: + p.panicf("Key '%s' has already been defined.", keyContext) + } + } + keyContext = append(keyContext, key) + + if _, ok := hash[key]; ok { + // Normally redefining keys isn't allowed, but the key could have been + // defined implicitly and it's allowed to be redefined concretely. (See + // the `valid/implicit-and-explicit-after.toml` in toml-test) + // + // But we have to make sure to stop marking it as an implicit. (So that + // another redefinition provokes an error.) + // + // Note that since it has already been defined (as a hash), we don't + // want to overwrite it. So our business is done. + if p.isArray(keyContext) { + p.removeImplicit(keyContext) + hash[key] = value + return + } + if p.isImplicit(keyContext) { + p.removeImplicit(keyContext) + return + } + // Otherwise, we have a concrete key trying to override a previous key, + // which is *always* wrong. + p.panicf("Key '%s' has already been defined.", keyContext) + } + + hash[key] = value +} + +// setType sets the type of a particular value at a given key. It should be +// called immediately AFTER setValue. +// +// Note that if `key` is empty, then the type given will be applied to the +// current context (which is either a table or an array of tables). +func (p *parser) setType(key string, typ tomlType, pos Position) { + keyContext := make(Key, 0, len(p.context)+1) + keyContext = append(keyContext, p.context...) + if len(key) > 0 { // allow type setting for hashes + keyContext = append(keyContext, key) + } + // Special case to make empty keys ("" = 1) work. + // Without it it will set "" rather than `""`. + // TODO: why is this needed? And why is this only needed here? + if len(keyContext) == 0 { + keyContext = Key{""} + } + p.keyInfo[keyContext.String()] = keyInfo{tomlType: typ, pos: pos} +} + +// Implicit keys need to be created when tables are implied in "a.b.c.d = 1" and +// "[a.b.c]" (the "a", "b", and "c" hashes are never created explicitly). +func (p *parser) addImplicit(key Key) { p.implicits[key.String()] = struct{}{} } +func (p *parser) removeImplicit(key Key) { delete(p.implicits, key.String()) } +func (p *parser) isImplicit(key Key) bool { _, ok := p.implicits[key.String()]; return ok } +func (p *parser) isArray(key Key) bool { return p.keyInfo[key.String()].tomlType == tomlArray } +func (p *parser) addImplicitContext(key Key) { p.addImplicit(key); p.addContext(key, false) } + +// current returns the full key name of the current context. +func (p *parser) current() string { + if len(p.currentKey) == 0 { + return p.context.String() + } + if len(p.context) == 0 { + return p.currentKey + } + return fmt.Sprintf("%s.%s", p.context, p.currentKey) +} + +func stripFirstNewline(s string) string { + if len(s) > 0 && s[0] == '\n' { + return s[1:] + } + if len(s) > 1 && s[0] == '\r' && s[1] == '\n' { + return s[2:] + } + return s +} + +// stripEscapedNewlines removes whitespace after line-ending backslashes in +// multiline strings. +// +// A line-ending backslash is an unescaped \ followed only by whitespace until +// the next newline. After a line-ending backslash, all whitespace is removed +// until the next non-whitespace character. +func (p *parser) stripEscapedNewlines(s string) string { + var ( + b strings.Builder + i int + ) + b.Grow(len(s)) + for { + ix := strings.Index(s[i:], `\`) + if ix < 0 { + b.WriteString(s) + return b.String() + } + i += ix + + if len(s) > i+1 && s[i+1] == '\\' { + // Escaped backslash. + i += 2 + continue + } + // Scan until the next non-whitespace. + j := i + 1 + whitespaceLoop: + for ; j < len(s); j++ { + switch s[j] { + case ' ', '\t', '\r', '\n': + default: + break whitespaceLoop + } + } + if j == i+1 { + // Not a whitespace escape. + i++ + continue + } + if !strings.Contains(s[i:j], "\n") { + // This is not a line-ending backslash. (It's a bad escape sequence, + // but we can let replaceEscapes catch it.) + i++ + continue + } + b.WriteString(s[:i]) + s = s[j:] + i = 0 + } +} + +func (p *parser) replaceEscapes(it item, str string) string { + var ( + b strings.Builder + skip = 0 + ) + b.Grow(len(str)) + for i, c := range str { + if skip > 0 { + skip-- + continue + } + if c != '\\' { + b.WriteRune(c) + continue + } + + if i >= len(str) { + p.bug("Escape sequence at end of string.") + return "" + } + switch str[i+1] { + default: + p.bug("Expected valid escape code after \\, but got %q.", str[i+1]) + case ' ', '\t': + p.panicItemf(it, "invalid escape: '\\%c'", str[i+1]) + case 'b': + b.WriteByte(0x08) + skip = 1 + case 't': + b.WriteByte(0x09) + skip = 1 + case 'n': + b.WriteByte(0x0a) + skip = 1 + case 'f': + b.WriteByte(0x0c) + skip = 1 + case 'r': + b.WriteByte(0x0d) + skip = 1 + case 'e': + if p.tomlNext { + b.WriteByte(0x1b) + skip = 1 + } + case '"': + b.WriteByte(0x22) + skip = 1 + case '\\': + b.WriteByte(0x5c) + skip = 1 + // The lexer guarantees the correct number of characters are present; + // don't need to check here. + case 'x': + if p.tomlNext { + escaped := p.asciiEscapeToUnicode(it, str[i+2:i+4]) + b.WriteRune(escaped) + skip = 3 + } + case 'u': + escaped := p.asciiEscapeToUnicode(it, str[i+2:i+6]) + b.WriteRune(escaped) + skip = 5 + case 'U': + escaped := p.asciiEscapeToUnicode(it, str[i+2:i+10]) + b.WriteRune(escaped) + skip = 9 + } + } + return b.String() +} + +func (p *parser) asciiEscapeToUnicode(it item, s string) rune { + hex, err := strconv.ParseUint(strings.ToLower(s), 16, 32) + if err != nil { + p.bug("Could not parse '%s' as a hexadecimal number, but the lexer claims it's OK: %s", s, err) + } + if !utf8.ValidRune(rune(hex)) { + p.panicItemf(it, "Escaped character '\\u%s' is not valid UTF-8.", s) + } + return rune(hex) +} diff --git a/vendor/github.com/BurntSushi/toml/type_fields.go b/vendor/github.com/BurntSushi/toml/type_fields.go new file mode 100644 index 0000000..10c51f7 --- /dev/null +++ b/vendor/github.com/BurntSushi/toml/type_fields.go @@ -0,0 +1,238 @@ +package toml + +// Struct field handling is adapted from code in encoding/json: +// +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the Go distribution. + +import ( + "reflect" + "sort" + "sync" +) + +// A field represents a single field found in a struct. +type field struct { + name string // the name of the field (`toml` tag included) + tag bool // whether field has a `toml` tag + index []int // represents the depth of an anonymous field + typ reflect.Type // the type of the field +} + +// byName sorts field by name, breaking ties with depth, +// then breaking ties with "name came from toml tag", then +// breaking ties with index sequence. +type byName []field + +func (x byName) Len() int { return len(x) } +func (x byName) Swap(i, j int) { x[i], x[j] = x[j], x[i] } +func (x byName) Less(i, j int) bool { + if x[i].name != x[j].name { + return x[i].name < x[j].name + } + if len(x[i].index) != len(x[j].index) { + return len(x[i].index) < len(x[j].index) + } + if x[i].tag != x[j].tag { + return x[i].tag + } + return byIndex(x).Less(i, j) +} + +// byIndex sorts field by index sequence. +type byIndex []field + +func (x byIndex) Len() int { return len(x) } +func (x byIndex) Swap(i, j int) { x[i], x[j] = x[j], x[i] } +func (x byIndex) Less(i, j int) bool { + for k, xik := range x[i].index { + if k >= len(x[j].index) { + return false + } + if xik != x[j].index[k] { + return xik < x[j].index[k] + } + } + return len(x[i].index) < len(x[j].index) +} + +// typeFields returns a list of fields that TOML should recognize for the given +// type. The algorithm is breadth-first search over the set of structs to +// include - the top struct and then any reachable anonymous structs. +func typeFields(t reflect.Type) []field { + // Anonymous fields to explore at the current level and the next. + current := []field{} + next := []field{{typ: t}} + + // Count of queued names for current level and the next. + var count map[reflect.Type]int + var nextCount map[reflect.Type]int + + // Types already visited at an earlier level. + visited := map[reflect.Type]bool{} + + // Fields found. + var fields []field + + for len(next) > 0 { + current, next = next, current[:0] + count, nextCount = nextCount, map[reflect.Type]int{} + + for _, f := range current { + if visited[f.typ] { + continue + } + visited[f.typ] = true + + // Scan f.typ for fields to include. + for i := 0; i < f.typ.NumField(); i++ { + sf := f.typ.Field(i) + if sf.PkgPath != "" && !sf.Anonymous { // unexported + continue + } + opts := getOptions(sf.Tag) + if opts.skip { + continue + } + index := make([]int, len(f.index)+1) + copy(index, f.index) + index[len(f.index)] = i + + ft := sf.Type + if ft.Name() == "" && ft.Kind() == reflect.Ptr { + // Follow pointer. + ft = ft.Elem() + } + + // Record found field and index sequence. + if opts.name != "" || !sf.Anonymous || ft.Kind() != reflect.Struct { + tagged := opts.name != "" + name := opts.name + if name == "" { + name = sf.Name + } + fields = append(fields, field{name, tagged, index, ft}) + if count[f.typ] > 1 { + // If there were multiple instances, add a second, + // so that the annihilation code will see a duplicate. + // It only cares about the distinction between 1 or 2, + // so don't bother generating any more copies. + fields = append(fields, fields[len(fields)-1]) + } + continue + } + + // Record new anonymous struct to explore in next round. + nextCount[ft]++ + if nextCount[ft] == 1 { + f := field{name: ft.Name(), index: index, typ: ft} + next = append(next, f) + } + } + } + } + + sort.Sort(byName(fields)) + + // Delete all fields that are hidden by the Go rules for embedded fields, + // except that fields with TOML tags are promoted. + + // The fields are sorted in primary order of name, secondary order + // of field index length. Loop over names; for each name, delete + // hidden fields by choosing the one dominant field that survives. + out := fields[:0] + for advance, i := 0, 0; i < len(fields); i += advance { + // One iteration per name. + // Find the sequence of fields with the name of this first field. + fi := fields[i] + name := fi.name + for advance = 1; i+advance < len(fields); advance++ { + fj := fields[i+advance] + if fj.name != name { + break + } + } + if advance == 1 { // Only one field with this name + out = append(out, fi) + continue + } + dominant, ok := dominantField(fields[i : i+advance]) + if ok { + out = append(out, dominant) + } + } + + fields = out + sort.Sort(byIndex(fields)) + + return fields +} + +// dominantField looks through the fields, all of which are known to +// have the same name, to find the single field that dominates the +// others using Go's embedding rules, modified by the presence of +// TOML tags. If there are multiple top-level fields, the boolean +// will be false: This condition is an error in Go and we skip all +// the fields. +func dominantField(fields []field) (field, bool) { + // The fields are sorted in increasing index-length order. The winner + // must therefore be one with the shortest index length. Drop all + // longer entries, which is easy: just truncate the slice. + length := len(fields[0].index) + tagged := -1 // Index of first tagged field. + for i, f := range fields { + if len(f.index) > length { + fields = fields[:i] + break + } + if f.tag { + if tagged >= 0 { + // Multiple tagged fields at the same level: conflict. + // Return no field. + return field{}, false + } + tagged = i + } + } + if tagged >= 0 { + return fields[tagged], true + } + // All remaining fields have the same length. If there's more than one, + // we have a conflict (two fields named "X" at the same level) and we + // return no field. + if len(fields) > 1 { + return field{}, false + } + return fields[0], true +} + +var fieldCache struct { + sync.RWMutex + m map[reflect.Type][]field +} + +// cachedTypeFields is like typeFields but uses a cache to avoid repeated work. +func cachedTypeFields(t reflect.Type) []field { + fieldCache.RLock() + f := fieldCache.m[t] + fieldCache.RUnlock() + if f != nil { + return f + } + + // Compute fields without lock. + // Might duplicate effort but won't hold other computations back. + f = typeFields(t) + if f == nil { + f = []field{} + } + + fieldCache.Lock() + if fieldCache.m == nil { + fieldCache.m = map[reflect.Type][]field{} + } + fieldCache.m[t] = f + fieldCache.Unlock() + return f +} diff --git a/vendor/github.com/BurntSushi/toml/type_toml.go b/vendor/github.com/BurntSushi/toml/type_toml.go new file mode 100644 index 0000000..1c090d3 --- /dev/null +++ b/vendor/github.com/BurntSushi/toml/type_toml.go @@ -0,0 +1,65 @@ +package toml + +// tomlType represents any Go type that corresponds to a TOML type. +// While the first draft of the TOML spec has a simplistic type system that +// probably doesn't need this level of sophistication, we seem to be militating +// toward adding real composite types. +type tomlType interface { + typeString() string +} + +// typeEqual accepts any two types and returns true if they are equal. +func typeEqual(t1, t2 tomlType) bool { + if t1 == nil || t2 == nil { + return false + } + return t1.typeString() == t2.typeString() +} + +func typeIsTable(t tomlType) bool { + return typeEqual(t, tomlHash) || typeEqual(t, tomlArrayHash) +} + +type tomlBaseType string + +func (btype tomlBaseType) typeString() string { return string(btype) } +func (btype tomlBaseType) String() string { return btype.typeString() } + +var ( + tomlInteger tomlBaseType = "Integer" + tomlFloat tomlBaseType = "Float" + tomlDatetime tomlBaseType = "Datetime" + tomlString tomlBaseType = "String" + tomlBool tomlBaseType = "Bool" + tomlArray tomlBaseType = "Array" + tomlHash tomlBaseType = "Hash" + tomlArrayHash tomlBaseType = "ArrayHash" +) + +// typeOfPrimitive returns a tomlType of any primitive value in TOML. +// Primitive values are: Integer, Float, Datetime, String and Bool. +// +// Passing a lexer item other than the following will cause a BUG message +// to occur: itemString, itemBool, itemInteger, itemFloat, itemDatetime. +func (p *parser) typeOfPrimitive(lexItem item) tomlType { + switch lexItem.typ { + case itemInteger: + return tomlInteger + case itemFloat: + return tomlFloat + case itemDatetime: + return tomlDatetime + case itemString, itemStringEsc: + return tomlString + case itemMultilineString: + return tomlString + case itemRawString: + return tomlString + case itemRawMultilineString: + return tomlString + case itemBool: + return tomlBool + } + p.bug("Cannot infer primitive type of lex item '%s'.", lexItem) + panic("unreachable") +} diff --git a/vendor/github.com/davecgh/go-spew/LICENSE b/vendor/github.com/davecgh/go-spew/LICENSE new file mode 100644 index 0000000..bc52e96 --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/LICENSE @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2012-2016 Dave Collins + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/vendor/github.com/davecgh/go-spew/spew/bypass.go b/vendor/github.com/davecgh/go-spew/spew/bypass.go new file mode 100644 index 0000000..7929947 --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/spew/bypass.go @@ -0,0 +1,145 @@ +// Copyright (c) 2015-2016 Dave Collins +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +// NOTE: Due to the following build constraints, this file will only be compiled +// when the code is not running on Google App Engine, compiled by GopherJS, and +// "-tags safe" is not added to the go build command line. The "disableunsafe" +// tag is deprecated and thus should not be used. +// Go versions prior to 1.4 are disabled because they use a different layout +// for interfaces which make the implementation of unsafeReflectValue more complex. +// +build !js,!appengine,!safe,!disableunsafe,go1.4 + +package spew + +import ( + "reflect" + "unsafe" +) + +const ( + // UnsafeDisabled is a build-time constant which specifies whether or + // not access to the unsafe package is available. + UnsafeDisabled = false + + // ptrSize is the size of a pointer on the current arch. + ptrSize = unsafe.Sizeof((*byte)(nil)) +) + +type flag uintptr + +var ( + // flagRO indicates whether the value field of a reflect.Value + // is read-only. + flagRO flag + + // flagAddr indicates whether the address of the reflect.Value's + // value may be taken. + flagAddr flag +) + +// flagKindMask holds the bits that make up the kind +// part of the flags field. In all the supported versions, +// it is in the lower 5 bits. +const flagKindMask = flag(0x1f) + +// Different versions of Go have used different +// bit layouts for the flags type. This table +// records the known combinations. +var okFlags = []struct { + ro, addr flag +}{{ + // From Go 1.4 to 1.5 + ro: 1 << 5, + addr: 1 << 7, +}, { + // Up to Go tip. + ro: 1<<5 | 1<<6, + addr: 1 << 8, +}} + +var flagValOffset = func() uintptr { + field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag") + if !ok { + panic("reflect.Value has no flag field") + } + return field.Offset +}() + +// flagField returns a pointer to the flag field of a reflect.Value. +func flagField(v *reflect.Value) *flag { + return (*flag)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + flagValOffset)) +} + +// unsafeReflectValue converts the passed reflect.Value into a one that bypasses +// the typical safety restrictions preventing access to unaddressable and +// unexported data. It works by digging the raw pointer to the underlying +// value out of the protected value and generating a new unprotected (unsafe) +// reflect.Value to it. +// +// This allows us to check for implementations of the Stringer and error +// interfaces to be used for pretty printing ordinarily unaddressable and +// inaccessible values such as unexported struct fields. +func unsafeReflectValue(v reflect.Value) reflect.Value { + if !v.IsValid() || (v.CanInterface() && v.CanAddr()) { + return v + } + flagFieldPtr := flagField(&v) + *flagFieldPtr &^= flagRO + *flagFieldPtr |= flagAddr + return v +} + +// Sanity checks against future reflect package changes +// to the type or semantics of the Value.flag field. +func init() { + field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag") + if !ok { + panic("reflect.Value has no flag field") + } + if field.Type.Kind() != reflect.TypeOf(flag(0)).Kind() { + panic("reflect.Value flag field has changed kind") + } + type t0 int + var t struct { + A t0 + // t0 will have flagEmbedRO set. + t0 + // a will have flagStickyRO set + a t0 + } + vA := reflect.ValueOf(t).FieldByName("A") + va := reflect.ValueOf(t).FieldByName("a") + vt0 := reflect.ValueOf(t).FieldByName("t0") + + // Infer flagRO from the difference between the flags + // for the (otherwise identical) fields in t. + flagPublic := *flagField(&vA) + flagWithRO := *flagField(&va) | *flagField(&vt0) + flagRO = flagPublic ^ flagWithRO + + // Infer flagAddr from the difference between a value + // taken from a pointer and not. + vPtrA := reflect.ValueOf(&t).Elem().FieldByName("A") + flagNoPtr := *flagField(&vA) + flagPtr := *flagField(&vPtrA) + flagAddr = flagNoPtr ^ flagPtr + + // Check that the inferred flags tally with one of the known versions. + for _, f := range okFlags { + if flagRO == f.ro && flagAddr == f.addr { + return + } + } + panic("reflect.Value read-only flag has changed semantics") +} diff --git a/vendor/github.com/davecgh/go-spew/spew/bypasssafe.go b/vendor/github.com/davecgh/go-spew/spew/bypasssafe.go new file mode 100644 index 0000000..205c28d --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/spew/bypasssafe.go @@ -0,0 +1,38 @@ +// Copyright (c) 2015-2016 Dave Collins +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +// NOTE: Due to the following build constraints, this file will only be compiled +// when the code is running on Google App Engine, compiled by GopherJS, or +// "-tags safe" is added to the go build command line. The "disableunsafe" +// tag is deprecated and thus should not be used. +// +build js appengine safe disableunsafe !go1.4 + +package spew + +import "reflect" + +const ( + // UnsafeDisabled is a build-time constant which specifies whether or + // not access to the unsafe package is available. + UnsafeDisabled = true +) + +// unsafeReflectValue typically converts the passed reflect.Value into a one +// that bypasses the typical safety restrictions preventing access to +// unaddressable and unexported data. However, doing this relies on access to +// the unsafe package. This is a stub version which simply returns the passed +// reflect.Value when the unsafe package is not available. +func unsafeReflectValue(v reflect.Value) reflect.Value { + return v +} diff --git a/vendor/github.com/davecgh/go-spew/spew/common.go b/vendor/github.com/davecgh/go-spew/spew/common.go new file mode 100644 index 0000000..1be8ce9 --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/spew/common.go @@ -0,0 +1,341 @@ +/* + * Copyright (c) 2013-2016 Dave Collins + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package spew + +import ( + "bytes" + "fmt" + "io" + "reflect" + "sort" + "strconv" +) + +// Some constants in the form of bytes to avoid string overhead. This mirrors +// the technique used in the fmt package. +var ( + panicBytes = []byte("(PANIC=") + plusBytes = []byte("+") + iBytes = []byte("i") + trueBytes = []byte("true") + falseBytes = []byte("false") + interfaceBytes = []byte("(interface {})") + commaNewlineBytes = []byte(",\n") + newlineBytes = []byte("\n") + openBraceBytes = []byte("{") + openBraceNewlineBytes = []byte("{\n") + closeBraceBytes = []byte("}") + asteriskBytes = []byte("*") + colonBytes = []byte(":") + colonSpaceBytes = []byte(": ") + openParenBytes = []byte("(") + closeParenBytes = []byte(")") + spaceBytes = []byte(" ") + pointerChainBytes = []byte("->") + nilAngleBytes = []byte("") + maxNewlineBytes = []byte("\n") + maxShortBytes = []byte("") + circularBytes = []byte("") + circularShortBytes = []byte("") + invalidAngleBytes = []byte("") + openBracketBytes = []byte("[") + closeBracketBytes = []byte("]") + percentBytes = []byte("%") + precisionBytes = []byte(".") + openAngleBytes = []byte("<") + closeAngleBytes = []byte(">") + openMapBytes = []byte("map[") + closeMapBytes = []byte("]") + lenEqualsBytes = []byte("len=") + capEqualsBytes = []byte("cap=") +) + +// hexDigits is used to map a decimal value to a hex digit. +var hexDigits = "0123456789abcdef" + +// catchPanic handles any panics that might occur during the handleMethods +// calls. +func catchPanic(w io.Writer, v reflect.Value) { + if err := recover(); err != nil { + w.Write(panicBytes) + fmt.Fprintf(w, "%v", err) + w.Write(closeParenBytes) + } +} + +// handleMethods attempts to call the Error and String methods on the underlying +// type the passed reflect.Value represents and outputes the result to Writer w. +// +// It handles panics in any called methods by catching and displaying the error +// as the formatted value. +func handleMethods(cs *ConfigState, w io.Writer, v reflect.Value) (handled bool) { + // We need an interface to check if the type implements the error or + // Stringer interface. However, the reflect package won't give us an + // interface on certain things like unexported struct fields in order + // to enforce visibility rules. We use unsafe, when it's available, + // to bypass these restrictions since this package does not mutate the + // values. + if !v.CanInterface() { + if UnsafeDisabled { + return false + } + + v = unsafeReflectValue(v) + } + + // Choose whether or not to do error and Stringer interface lookups against + // the base type or a pointer to the base type depending on settings. + // Technically calling one of these methods with a pointer receiver can + // mutate the value, however, types which choose to satisify an error or + // Stringer interface with a pointer receiver should not be mutating their + // state inside these interface methods. + if !cs.DisablePointerMethods && !UnsafeDisabled && !v.CanAddr() { + v = unsafeReflectValue(v) + } + if v.CanAddr() { + v = v.Addr() + } + + // Is it an error or Stringer? + switch iface := v.Interface().(type) { + case error: + defer catchPanic(w, v) + if cs.ContinueOnMethod { + w.Write(openParenBytes) + w.Write([]byte(iface.Error())) + w.Write(closeParenBytes) + w.Write(spaceBytes) + return false + } + + w.Write([]byte(iface.Error())) + return true + + case fmt.Stringer: + defer catchPanic(w, v) + if cs.ContinueOnMethod { + w.Write(openParenBytes) + w.Write([]byte(iface.String())) + w.Write(closeParenBytes) + w.Write(spaceBytes) + return false + } + w.Write([]byte(iface.String())) + return true + } + return false +} + +// printBool outputs a boolean value as true or false to Writer w. +func printBool(w io.Writer, val bool) { + if val { + w.Write(trueBytes) + } else { + w.Write(falseBytes) + } +} + +// printInt outputs a signed integer value to Writer w. +func printInt(w io.Writer, val int64, base int) { + w.Write([]byte(strconv.FormatInt(val, base))) +} + +// printUint outputs an unsigned integer value to Writer w. +func printUint(w io.Writer, val uint64, base int) { + w.Write([]byte(strconv.FormatUint(val, base))) +} + +// printFloat outputs a floating point value using the specified precision, +// which is expected to be 32 or 64bit, to Writer w. +func printFloat(w io.Writer, val float64, precision int) { + w.Write([]byte(strconv.FormatFloat(val, 'g', -1, precision))) +} + +// printComplex outputs a complex value using the specified float precision +// for the real and imaginary parts to Writer w. +func printComplex(w io.Writer, c complex128, floatPrecision int) { + r := real(c) + w.Write(openParenBytes) + w.Write([]byte(strconv.FormatFloat(r, 'g', -1, floatPrecision))) + i := imag(c) + if i >= 0 { + w.Write(plusBytes) + } + w.Write([]byte(strconv.FormatFloat(i, 'g', -1, floatPrecision))) + w.Write(iBytes) + w.Write(closeParenBytes) +} + +// printHexPtr outputs a uintptr formatted as hexadecimal with a leading '0x' +// prefix to Writer w. +func printHexPtr(w io.Writer, p uintptr) { + // Null pointer. + num := uint64(p) + if num == 0 { + w.Write(nilAngleBytes) + return + } + + // Max uint64 is 16 bytes in hex + 2 bytes for '0x' prefix + buf := make([]byte, 18) + + // It's simpler to construct the hex string right to left. + base := uint64(16) + i := len(buf) - 1 + for num >= base { + buf[i] = hexDigits[num%base] + num /= base + i-- + } + buf[i] = hexDigits[num] + + // Add '0x' prefix. + i-- + buf[i] = 'x' + i-- + buf[i] = '0' + + // Strip unused leading bytes. + buf = buf[i:] + w.Write(buf) +} + +// valuesSorter implements sort.Interface to allow a slice of reflect.Value +// elements to be sorted. +type valuesSorter struct { + values []reflect.Value + strings []string // either nil or same len and values + cs *ConfigState +} + +// newValuesSorter initializes a valuesSorter instance, which holds a set of +// surrogate keys on which the data should be sorted. It uses flags in +// ConfigState to decide if and how to populate those surrogate keys. +func newValuesSorter(values []reflect.Value, cs *ConfigState) sort.Interface { + vs := &valuesSorter{values: values, cs: cs} + if canSortSimply(vs.values[0].Kind()) { + return vs + } + if !cs.DisableMethods { + vs.strings = make([]string, len(values)) + for i := range vs.values { + b := bytes.Buffer{} + if !handleMethods(cs, &b, vs.values[i]) { + vs.strings = nil + break + } + vs.strings[i] = b.String() + } + } + if vs.strings == nil && cs.SpewKeys { + vs.strings = make([]string, len(values)) + for i := range vs.values { + vs.strings[i] = Sprintf("%#v", vs.values[i].Interface()) + } + } + return vs +} + +// canSortSimply tests whether a reflect.Kind is a primitive that can be sorted +// directly, or whether it should be considered for sorting by surrogate keys +// (if the ConfigState allows it). +func canSortSimply(kind reflect.Kind) bool { + // This switch parallels valueSortLess, except for the default case. + switch kind { + case reflect.Bool: + return true + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: + return true + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: + return true + case reflect.Float32, reflect.Float64: + return true + case reflect.String: + return true + case reflect.Uintptr: + return true + case reflect.Array: + return true + } + return false +} + +// Len returns the number of values in the slice. It is part of the +// sort.Interface implementation. +func (s *valuesSorter) Len() int { + return len(s.values) +} + +// Swap swaps the values at the passed indices. It is part of the +// sort.Interface implementation. +func (s *valuesSorter) Swap(i, j int) { + s.values[i], s.values[j] = s.values[j], s.values[i] + if s.strings != nil { + s.strings[i], s.strings[j] = s.strings[j], s.strings[i] + } +} + +// valueSortLess returns whether the first value should sort before the second +// value. It is used by valueSorter.Less as part of the sort.Interface +// implementation. +func valueSortLess(a, b reflect.Value) bool { + switch a.Kind() { + case reflect.Bool: + return !a.Bool() && b.Bool() + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: + return a.Int() < b.Int() + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: + return a.Uint() < b.Uint() + case reflect.Float32, reflect.Float64: + return a.Float() < b.Float() + case reflect.String: + return a.String() < b.String() + case reflect.Uintptr: + return a.Uint() < b.Uint() + case reflect.Array: + // Compare the contents of both arrays. + l := a.Len() + for i := 0; i < l; i++ { + av := a.Index(i) + bv := b.Index(i) + if av.Interface() == bv.Interface() { + continue + } + return valueSortLess(av, bv) + } + } + return a.String() < b.String() +} + +// Less returns whether the value at index i should sort before the +// value at index j. It is part of the sort.Interface implementation. +func (s *valuesSorter) Less(i, j int) bool { + if s.strings == nil { + return valueSortLess(s.values[i], s.values[j]) + } + return s.strings[i] < s.strings[j] +} + +// sortValues is a sort function that handles both native types and any type that +// can be converted to error or Stringer. Other inputs are sorted according to +// their Value.String() value to ensure display stability. +func sortValues(values []reflect.Value, cs *ConfigState) { + if len(values) == 0 { + return + } + sort.Sort(newValuesSorter(values, cs)) +} diff --git a/vendor/github.com/davecgh/go-spew/spew/config.go b/vendor/github.com/davecgh/go-spew/spew/config.go new file mode 100644 index 0000000..2e3d22f --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/spew/config.go @@ -0,0 +1,306 @@ +/* + * Copyright (c) 2013-2016 Dave Collins + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package spew + +import ( + "bytes" + "fmt" + "io" + "os" +) + +// ConfigState houses the configuration options used by spew to format and +// display values. There is a global instance, Config, that is used to control +// all top-level Formatter and Dump functionality. Each ConfigState instance +// provides methods equivalent to the top-level functions. +// +// The zero value for ConfigState provides no indentation. You would typically +// want to set it to a space or a tab. +// +// Alternatively, you can use NewDefaultConfig to get a ConfigState instance +// with default settings. See the documentation of NewDefaultConfig for default +// values. +type ConfigState struct { + // Indent specifies the string to use for each indentation level. The + // global config instance that all top-level functions use set this to a + // single space by default. If you would like more indentation, you might + // set this to a tab with "\t" or perhaps two spaces with " ". + Indent string + + // MaxDepth controls the maximum number of levels to descend into nested + // data structures. The default, 0, means there is no limit. + // + // NOTE: Circular data structures are properly detected, so it is not + // necessary to set this value unless you specifically want to limit deeply + // nested data structures. + MaxDepth int + + // DisableMethods specifies whether or not error and Stringer interfaces are + // invoked for types that implement them. + DisableMethods bool + + // DisablePointerMethods specifies whether or not to check for and invoke + // error and Stringer interfaces on types which only accept a pointer + // receiver when the current type is not a pointer. + // + // NOTE: This might be an unsafe action since calling one of these methods + // with a pointer receiver could technically mutate the value, however, + // in practice, types which choose to satisify an error or Stringer + // interface with a pointer receiver should not be mutating their state + // inside these interface methods. As a result, this option relies on + // access to the unsafe package, so it will not have any effect when + // running in environments without access to the unsafe package such as + // Google App Engine or with the "safe" build tag specified. + DisablePointerMethods bool + + // DisablePointerAddresses specifies whether to disable the printing of + // pointer addresses. This is useful when diffing data structures in tests. + DisablePointerAddresses bool + + // DisableCapacities specifies whether to disable the printing of capacities + // for arrays, slices, maps and channels. This is useful when diffing + // data structures in tests. + DisableCapacities bool + + // ContinueOnMethod specifies whether or not recursion should continue once + // a custom error or Stringer interface is invoked. The default, false, + // means it will print the results of invoking the custom error or Stringer + // interface and return immediately instead of continuing to recurse into + // the internals of the data type. + // + // NOTE: This flag does not have any effect if method invocation is disabled + // via the DisableMethods or DisablePointerMethods options. + ContinueOnMethod bool + + // SortKeys specifies map keys should be sorted before being printed. Use + // this to have a more deterministic, diffable output. Note that only + // native types (bool, int, uint, floats, uintptr and string) and types + // that support the error or Stringer interfaces (if methods are + // enabled) are supported, with other types sorted according to the + // reflect.Value.String() output which guarantees display stability. + SortKeys bool + + // SpewKeys specifies that, as a last resort attempt, map keys should + // be spewed to strings and sorted by those strings. This is only + // considered if SortKeys is true. + SpewKeys bool +} + +// Config is the active configuration of the top-level functions. +// The configuration can be changed by modifying the contents of spew.Config. +var Config = ConfigState{Indent: " "} + +// Errorf is a wrapper for fmt.Errorf that treats each argument as if it were +// passed with a Formatter interface returned by c.NewFormatter. It returns +// the formatted string as a value that satisfies error. See NewFormatter +// for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Errorf(format, c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Errorf(format string, a ...interface{}) (err error) { + return fmt.Errorf(format, c.convertArgs(a)...) +} + +// Fprint is a wrapper for fmt.Fprint that treats each argument as if it were +// passed with a Formatter interface returned by c.NewFormatter. It returns +// the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Fprint(w, c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Fprint(w io.Writer, a ...interface{}) (n int, err error) { + return fmt.Fprint(w, c.convertArgs(a)...) +} + +// Fprintf is a wrapper for fmt.Fprintf that treats each argument as if it were +// passed with a Formatter interface returned by c.NewFormatter. It returns +// the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Fprintf(w, format, c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) { + return fmt.Fprintf(w, format, c.convertArgs(a)...) +} + +// Fprintln is a wrapper for fmt.Fprintln that treats each argument as if it +// passed with a Formatter interface returned by c.NewFormatter. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Fprintln(w, c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Fprintln(w io.Writer, a ...interface{}) (n int, err error) { + return fmt.Fprintln(w, c.convertArgs(a)...) +} + +// Print is a wrapper for fmt.Print that treats each argument as if it were +// passed with a Formatter interface returned by c.NewFormatter. It returns +// the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Print(c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Print(a ...interface{}) (n int, err error) { + return fmt.Print(c.convertArgs(a)...) +} + +// Printf is a wrapper for fmt.Printf that treats each argument as if it were +// passed with a Formatter interface returned by c.NewFormatter. It returns +// the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Printf(format, c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Printf(format string, a ...interface{}) (n int, err error) { + return fmt.Printf(format, c.convertArgs(a)...) +} + +// Println is a wrapper for fmt.Println that treats each argument as if it were +// passed with a Formatter interface returned by c.NewFormatter. It returns +// the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Println(c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Println(a ...interface{}) (n int, err error) { + return fmt.Println(c.convertArgs(a)...) +} + +// Sprint is a wrapper for fmt.Sprint that treats each argument as if it were +// passed with a Formatter interface returned by c.NewFormatter. It returns +// the resulting string. See NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Sprint(c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Sprint(a ...interface{}) string { + return fmt.Sprint(c.convertArgs(a)...) +} + +// Sprintf is a wrapper for fmt.Sprintf that treats each argument as if it were +// passed with a Formatter interface returned by c.NewFormatter. It returns +// the resulting string. See NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Sprintf(format, c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Sprintf(format string, a ...interface{}) string { + return fmt.Sprintf(format, c.convertArgs(a)...) +} + +// Sprintln is a wrapper for fmt.Sprintln that treats each argument as if it +// were passed with a Formatter interface returned by c.NewFormatter. It +// returns the resulting string. See NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Sprintln(c.NewFormatter(a), c.NewFormatter(b)) +func (c *ConfigState) Sprintln(a ...interface{}) string { + return fmt.Sprintln(c.convertArgs(a)...) +} + +/* +NewFormatter returns a custom formatter that satisfies the fmt.Formatter +interface. As a result, it integrates cleanly with standard fmt package +printing functions. The formatter is useful for inline printing of smaller data +types similar to the standard %v format specifier. + +The custom formatter only responds to the %v (most compact), %+v (adds pointer +addresses), %#v (adds types), and %#+v (adds types and pointer addresses) verb +combinations. Any other verbs such as %x and %q will be sent to the the +standard fmt package for formatting. In addition, the custom formatter ignores +the width and precision arguments (however they will still work on the format +specifiers not handled by the custom formatter). + +Typically this function shouldn't be called directly. It is much easier to make +use of the custom formatter by calling one of the convenience functions such as +c.Printf, c.Println, or c.Printf. +*/ +func (c *ConfigState) NewFormatter(v interface{}) fmt.Formatter { + return newFormatter(c, v) +} + +// Fdump formats and displays the passed arguments to io.Writer w. It formats +// exactly the same as Dump. +func (c *ConfigState) Fdump(w io.Writer, a ...interface{}) { + fdump(c, w, a...) +} + +/* +Dump displays the passed parameters to standard out with newlines, customizable +indentation, and additional debug information such as complete types and all +pointer addresses used to indirect to the final value. It provides the +following features over the built-in printing facilities provided by the fmt +package: + + * Pointers are dereferenced and followed + * Circular data structures are detected and handled properly + * Custom Stringer/error interfaces are optionally invoked, including + on unexported types + * Custom types which only implement the Stringer/error interfaces via + a pointer receiver are optionally invoked when passing non-pointer + variables + * Byte arrays and slices are dumped like the hexdump -C command which + includes offsets, byte values in hex, and ASCII output + +The configuration options are controlled by modifying the public members +of c. See ConfigState for options documentation. + +See Fdump if you would prefer dumping to an arbitrary io.Writer or Sdump to +get the formatted result as a string. +*/ +func (c *ConfigState) Dump(a ...interface{}) { + fdump(c, os.Stdout, a...) +} + +// Sdump returns a string with the passed arguments formatted exactly the same +// as Dump. +func (c *ConfigState) Sdump(a ...interface{}) string { + var buf bytes.Buffer + fdump(c, &buf, a...) + return buf.String() +} + +// convertArgs accepts a slice of arguments and returns a slice of the same +// length with each argument converted to a spew Formatter interface using +// the ConfigState associated with s. +func (c *ConfigState) convertArgs(args []interface{}) (formatters []interface{}) { + formatters = make([]interface{}, len(args)) + for index, arg := range args { + formatters[index] = newFormatter(c, arg) + } + return formatters +} + +// NewDefaultConfig returns a ConfigState with the following default settings. +// +// Indent: " " +// MaxDepth: 0 +// DisableMethods: false +// DisablePointerMethods: false +// ContinueOnMethod: false +// SortKeys: false +func NewDefaultConfig() *ConfigState { + return &ConfigState{Indent: " "} +} diff --git a/vendor/github.com/davecgh/go-spew/spew/doc.go b/vendor/github.com/davecgh/go-spew/spew/doc.go new file mode 100644 index 0000000..aacaac6 --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/spew/doc.go @@ -0,0 +1,211 @@ +/* + * Copyright (c) 2013-2016 Dave Collins + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* +Package spew implements a deep pretty printer for Go data structures to aid in +debugging. + +A quick overview of the additional features spew provides over the built-in +printing facilities for Go data types are as follows: + + * Pointers are dereferenced and followed + * Circular data structures are detected and handled properly + * Custom Stringer/error interfaces are optionally invoked, including + on unexported types + * Custom types which only implement the Stringer/error interfaces via + a pointer receiver are optionally invoked when passing non-pointer + variables + * Byte arrays and slices are dumped like the hexdump -C command which + includes offsets, byte values in hex, and ASCII output (only when using + Dump style) + +There are two different approaches spew allows for dumping Go data structures: + + * Dump style which prints with newlines, customizable indentation, + and additional debug information such as types and all pointer addresses + used to indirect to the final value + * A custom Formatter interface that integrates cleanly with the standard fmt + package and replaces %v, %+v, %#v, and %#+v to provide inline printing + similar to the default %v while providing the additional functionality + outlined above and passing unsupported format verbs such as %x and %q + along to fmt + +Quick Start + +This section demonstrates how to quickly get started with spew. See the +sections below for further details on formatting and configuration options. + +To dump a variable with full newlines, indentation, type, and pointer +information use Dump, Fdump, or Sdump: + spew.Dump(myVar1, myVar2, ...) + spew.Fdump(someWriter, myVar1, myVar2, ...) + str := spew.Sdump(myVar1, myVar2, ...) + +Alternatively, if you would prefer to use format strings with a compacted inline +printing style, use the convenience wrappers Printf, Fprintf, etc with +%v (most compact), %+v (adds pointer addresses), %#v (adds types), or +%#+v (adds types and pointer addresses): + spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2) + spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4) + spew.Fprintf(someWriter, "myVar1: %v -- myVar2: %+v", myVar1, myVar2) + spew.Fprintf(someWriter, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4) + +Configuration Options + +Configuration of spew is handled by fields in the ConfigState type. For +convenience, all of the top-level functions use a global state available +via the spew.Config global. + +It is also possible to create a ConfigState instance that provides methods +equivalent to the top-level functions. This allows concurrent configuration +options. See the ConfigState documentation for more details. + +The following configuration options are available: + * Indent + String to use for each indentation level for Dump functions. + It is a single space by default. A popular alternative is "\t". + + * MaxDepth + Maximum number of levels to descend into nested data structures. + There is no limit by default. + + * DisableMethods + Disables invocation of error and Stringer interface methods. + Method invocation is enabled by default. + + * DisablePointerMethods + Disables invocation of error and Stringer interface methods on types + which only accept pointer receivers from non-pointer variables. + Pointer method invocation is enabled by default. + + * DisablePointerAddresses + DisablePointerAddresses specifies whether to disable the printing of + pointer addresses. This is useful when diffing data structures in tests. + + * DisableCapacities + DisableCapacities specifies whether to disable the printing of + capacities for arrays, slices, maps and channels. This is useful when + diffing data structures in tests. + + * ContinueOnMethod + Enables recursion into types after invoking error and Stringer interface + methods. Recursion after method invocation is disabled by default. + + * SortKeys + Specifies map keys should be sorted before being printed. Use + this to have a more deterministic, diffable output. Note that + only native types (bool, int, uint, floats, uintptr and string) + and types which implement error or Stringer interfaces are + supported with other types sorted according to the + reflect.Value.String() output which guarantees display + stability. Natural map order is used by default. + + * SpewKeys + Specifies that, as a last resort attempt, map keys should be + spewed to strings and sorted by those strings. This is only + considered if SortKeys is true. + +Dump Usage + +Simply call spew.Dump with a list of variables you want to dump: + + spew.Dump(myVar1, myVar2, ...) + +You may also call spew.Fdump if you would prefer to output to an arbitrary +io.Writer. For example, to dump to standard error: + + spew.Fdump(os.Stderr, myVar1, myVar2, ...) + +A third option is to call spew.Sdump to get the formatted output as a string: + + str := spew.Sdump(myVar1, myVar2, ...) + +Sample Dump Output + +See the Dump example for details on the setup of the types and variables being +shown here. + + (main.Foo) { + unexportedField: (*main.Bar)(0xf84002e210)({ + flag: (main.Flag) flagTwo, + data: (uintptr) + }), + ExportedField: (map[interface {}]interface {}) (len=1) { + (string) (len=3) "one": (bool) true + } + } + +Byte (and uint8) arrays and slices are displayed uniquely like the hexdump -C +command as shown. + ([]uint8) (len=32 cap=32) { + 00000000 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 |............... | + 00000010 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 |!"#$%&'()*+,-./0| + 00000020 31 32 |12| + } + +Custom Formatter + +Spew provides a custom formatter that implements the fmt.Formatter interface +so that it integrates cleanly with standard fmt package printing functions. The +formatter is useful for inline printing of smaller data types similar to the +standard %v format specifier. + +The custom formatter only responds to the %v (most compact), %+v (adds pointer +addresses), %#v (adds types), or %#+v (adds types and pointer addresses) verb +combinations. Any other verbs such as %x and %q will be sent to the the +standard fmt package for formatting. In addition, the custom formatter ignores +the width and precision arguments (however they will still work on the format +specifiers not handled by the custom formatter). + +Custom Formatter Usage + +The simplest way to make use of the spew custom formatter is to call one of the +convenience functions such as spew.Printf, spew.Println, or spew.Printf. The +functions have syntax you are most likely already familiar with: + + spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2) + spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4) + spew.Println(myVar, myVar2) + spew.Fprintf(os.Stderr, "myVar1: %v -- myVar2: %+v", myVar1, myVar2) + spew.Fprintf(os.Stderr, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4) + +See the Index for the full list convenience functions. + +Sample Formatter Output + +Double pointer to a uint8: + %v: <**>5 + %+v: <**>(0xf8400420d0->0xf8400420c8)5 + %#v: (**uint8)5 + %#+v: (**uint8)(0xf8400420d0->0xf8400420c8)5 + +Pointer to circular struct with a uint8 field and a pointer to itself: + %v: <*>{1 <*>} + %+v: <*>(0xf84003e260){ui8:1 c:<*>(0xf84003e260)} + %#v: (*main.circular){ui8:(uint8)1 c:(*main.circular)} + %#+v: (*main.circular)(0xf84003e260){ui8:(uint8)1 c:(*main.circular)(0xf84003e260)} + +See the Printf example for details on the setup of variables being shown +here. + +Errors + +Since it is possible for custom Stringer/error interfaces to panic, spew +detects them and handles them internally by printing the panic information +inline with the output. Since spew is intended to provide deep pretty printing +capabilities on structures, it intentionally does not return any errors. +*/ +package spew diff --git a/vendor/github.com/davecgh/go-spew/spew/dump.go b/vendor/github.com/davecgh/go-spew/spew/dump.go new file mode 100644 index 0000000..f78d89f --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/spew/dump.go @@ -0,0 +1,509 @@ +/* + * Copyright (c) 2013-2016 Dave Collins + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package spew + +import ( + "bytes" + "encoding/hex" + "fmt" + "io" + "os" + "reflect" + "regexp" + "strconv" + "strings" +) + +var ( + // uint8Type is a reflect.Type representing a uint8. It is used to + // convert cgo types to uint8 slices for hexdumping. + uint8Type = reflect.TypeOf(uint8(0)) + + // cCharRE is a regular expression that matches a cgo char. + // It is used to detect character arrays to hexdump them. + cCharRE = regexp.MustCompile(`^.*\._Ctype_char$`) + + // cUnsignedCharRE is a regular expression that matches a cgo unsigned + // char. It is used to detect unsigned character arrays to hexdump + // them. + cUnsignedCharRE = regexp.MustCompile(`^.*\._Ctype_unsignedchar$`) + + // cUint8tCharRE is a regular expression that matches a cgo uint8_t. + // It is used to detect uint8_t arrays to hexdump them. + cUint8tCharRE = regexp.MustCompile(`^.*\._Ctype_uint8_t$`) +) + +// dumpState contains information about the state of a dump operation. +type dumpState struct { + w io.Writer + depth int + pointers map[uintptr]int + ignoreNextType bool + ignoreNextIndent bool + cs *ConfigState +} + +// indent performs indentation according to the depth level and cs.Indent +// option. +func (d *dumpState) indent() { + if d.ignoreNextIndent { + d.ignoreNextIndent = false + return + } + d.w.Write(bytes.Repeat([]byte(d.cs.Indent), d.depth)) +} + +// unpackValue returns values inside of non-nil interfaces when possible. +// This is useful for data types like structs, arrays, slices, and maps which +// can contain varying types packed inside an interface. +func (d *dumpState) unpackValue(v reflect.Value) reflect.Value { + if v.Kind() == reflect.Interface && !v.IsNil() { + v = v.Elem() + } + return v +} + +// dumpPtr handles formatting of pointers by indirecting them as necessary. +func (d *dumpState) dumpPtr(v reflect.Value) { + // Remove pointers at or below the current depth from map used to detect + // circular refs. + for k, depth := range d.pointers { + if depth >= d.depth { + delete(d.pointers, k) + } + } + + // Keep list of all dereferenced pointers to show later. + pointerChain := make([]uintptr, 0) + + // Figure out how many levels of indirection there are by dereferencing + // pointers and unpacking interfaces down the chain while detecting circular + // references. + nilFound := false + cycleFound := false + indirects := 0 + ve := v + for ve.Kind() == reflect.Ptr { + if ve.IsNil() { + nilFound = true + break + } + indirects++ + addr := ve.Pointer() + pointerChain = append(pointerChain, addr) + if pd, ok := d.pointers[addr]; ok && pd < d.depth { + cycleFound = true + indirects-- + break + } + d.pointers[addr] = d.depth + + ve = ve.Elem() + if ve.Kind() == reflect.Interface { + if ve.IsNil() { + nilFound = true + break + } + ve = ve.Elem() + } + } + + // Display type information. + d.w.Write(openParenBytes) + d.w.Write(bytes.Repeat(asteriskBytes, indirects)) + d.w.Write([]byte(ve.Type().String())) + d.w.Write(closeParenBytes) + + // Display pointer information. + if !d.cs.DisablePointerAddresses && len(pointerChain) > 0 { + d.w.Write(openParenBytes) + for i, addr := range pointerChain { + if i > 0 { + d.w.Write(pointerChainBytes) + } + printHexPtr(d.w, addr) + } + d.w.Write(closeParenBytes) + } + + // Display dereferenced value. + d.w.Write(openParenBytes) + switch { + case nilFound: + d.w.Write(nilAngleBytes) + + case cycleFound: + d.w.Write(circularBytes) + + default: + d.ignoreNextType = true + d.dump(ve) + } + d.w.Write(closeParenBytes) +} + +// dumpSlice handles formatting of arrays and slices. Byte (uint8 under +// reflection) arrays and slices are dumped in hexdump -C fashion. +func (d *dumpState) dumpSlice(v reflect.Value) { + // Determine whether this type should be hex dumped or not. Also, + // for types which should be hexdumped, try to use the underlying data + // first, then fall back to trying to convert them to a uint8 slice. + var buf []uint8 + doConvert := false + doHexDump := false + numEntries := v.Len() + if numEntries > 0 { + vt := v.Index(0).Type() + vts := vt.String() + switch { + // C types that need to be converted. + case cCharRE.MatchString(vts): + fallthrough + case cUnsignedCharRE.MatchString(vts): + fallthrough + case cUint8tCharRE.MatchString(vts): + doConvert = true + + // Try to use existing uint8 slices and fall back to converting + // and copying if that fails. + case vt.Kind() == reflect.Uint8: + // We need an addressable interface to convert the type + // to a byte slice. However, the reflect package won't + // give us an interface on certain things like + // unexported struct fields in order to enforce + // visibility rules. We use unsafe, when available, to + // bypass these restrictions since this package does not + // mutate the values. + vs := v + if !vs.CanInterface() || !vs.CanAddr() { + vs = unsafeReflectValue(vs) + } + if !UnsafeDisabled { + vs = vs.Slice(0, numEntries) + + // Use the existing uint8 slice if it can be + // type asserted. + iface := vs.Interface() + if slice, ok := iface.([]uint8); ok { + buf = slice + doHexDump = true + break + } + } + + // The underlying data needs to be converted if it can't + // be type asserted to a uint8 slice. + doConvert = true + } + + // Copy and convert the underlying type if needed. + if doConvert && vt.ConvertibleTo(uint8Type) { + // Convert and copy each element into a uint8 byte + // slice. + buf = make([]uint8, numEntries) + for i := 0; i < numEntries; i++ { + vv := v.Index(i) + buf[i] = uint8(vv.Convert(uint8Type).Uint()) + } + doHexDump = true + } + } + + // Hexdump the entire slice as needed. + if doHexDump { + indent := strings.Repeat(d.cs.Indent, d.depth) + str := indent + hex.Dump(buf) + str = strings.Replace(str, "\n", "\n"+indent, -1) + str = strings.TrimRight(str, d.cs.Indent) + d.w.Write([]byte(str)) + return + } + + // Recursively call dump for each item. + for i := 0; i < numEntries; i++ { + d.dump(d.unpackValue(v.Index(i))) + if i < (numEntries - 1) { + d.w.Write(commaNewlineBytes) + } else { + d.w.Write(newlineBytes) + } + } +} + +// dump is the main workhorse for dumping a value. It uses the passed reflect +// value to figure out what kind of object we are dealing with and formats it +// appropriately. It is a recursive function, however circular data structures +// are detected and handled properly. +func (d *dumpState) dump(v reflect.Value) { + // Handle invalid reflect values immediately. + kind := v.Kind() + if kind == reflect.Invalid { + d.w.Write(invalidAngleBytes) + return + } + + // Handle pointers specially. + if kind == reflect.Ptr { + d.indent() + d.dumpPtr(v) + return + } + + // Print type information unless already handled elsewhere. + if !d.ignoreNextType { + d.indent() + d.w.Write(openParenBytes) + d.w.Write([]byte(v.Type().String())) + d.w.Write(closeParenBytes) + d.w.Write(spaceBytes) + } + d.ignoreNextType = false + + // Display length and capacity if the built-in len and cap functions + // work with the value's kind and the len/cap itself is non-zero. + valueLen, valueCap := 0, 0 + switch v.Kind() { + case reflect.Array, reflect.Slice, reflect.Chan: + valueLen, valueCap = v.Len(), v.Cap() + case reflect.Map, reflect.String: + valueLen = v.Len() + } + if valueLen != 0 || !d.cs.DisableCapacities && valueCap != 0 { + d.w.Write(openParenBytes) + if valueLen != 0 { + d.w.Write(lenEqualsBytes) + printInt(d.w, int64(valueLen), 10) + } + if !d.cs.DisableCapacities && valueCap != 0 { + if valueLen != 0 { + d.w.Write(spaceBytes) + } + d.w.Write(capEqualsBytes) + printInt(d.w, int64(valueCap), 10) + } + d.w.Write(closeParenBytes) + d.w.Write(spaceBytes) + } + + // Call Stringer/error interfaces if they exist and the handle methods flag + // is enabled + if !d.cs.DisableMethods { + if (kind != reflect.Invalid) && (kind != reflect.Interface) { + if handled := handleMethods(d.cs, d.w, v); handled { + return + } + } + } + + switch kind { + case reflect.Invalid: + // Do nothing. We should never get here since invalid has already + // been handled above. + + case reflect.Bool: + printBool(d.w, v.Bool()) + + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: + printInt(d.w, v.Int(), 10) + + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: + printUint(d.w, v.Uint(), 10) + + case reflect.Float32: + printFloat(d.w, v.Float(), 32) + + case reflect.Float64: + printFloat(d.w, v.Float(), 64) + + case reflect.Complex64: + printComplex(d.w, v.Complex(), 32) + + case reflect.Complex128: + printComplex(d.w, v.Complex(), 64) + + case reflect.Slice: + if v.IsNil() { + d.w.Write(nilAngleBytes) + break + } + fallthrough + + case reflect.Array: + d.w.Write(openBraceNewlineBytes) + d.depth++ + if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) { + d.indent() + d.w.Write(maxNewlineBytes) + } else { + d.dumpSlice(v) + } + d.depth-- + d.indent() + d.w.Write(closeBraceBytes) + + case reflect.String: + d.w.Write([]byte(strconv.Quote(v.String()))) + + case reflect.Interface: + // The only time we should get here is for nil interfaces due to + // unpackValue calls. + if v.IsNil() { + d.w.Write(nilAngleBytes) + } + + case reflect.Ptr: + // Do nothing. We should never get here since pointers have already + // been handled above. + + case reflect.Map: + // nil maps should be indicated as different than empty maps + if v.IsNil() { + d.w.Write(nilAngleBytes) + break + } + + d.w.Write(openBraceNewlineBytes) + d.depth++ + if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) { + d.indent() + d.w.Write(maxNewlineBytes) + } else { + numEntries := v.Len() + keys := v.MapKeys() + if d.cs.SortKeys { + sortValues(keys, d.cs) + } + for i, key := range keys { + d.dump(d.unpackValue(key)) + d.w.Write(colonSpaceBytes) + d.ignoreNextIndent = true + d.dump(d.unpackValue(v.MapIndex(key))) + if i < (numEntries - 1) { + d.w.Write(commaNewlineBytes) + } else { + d.w.Write(newlineBytes) + } + } + } + d.depth-- + d.indent() + d.w.Write(closeBraceBytes) + + case reflect.Struct: + d.w.Write(openBraceNewlineBytes) + d.depth++ + if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) { + d.indent() + d.w.Write(maxNewlineBytes) + } else { + vt := v.Type() + numFields := v.NumField() + for i := 0; i < numFields; i++ { + d.indent() + vtf := vt.Field(i) + d.w.Write([]byte(vtf.Name)) + d.w.Write(colonSpaceBytes) + d.ignoreNextIndent = true + d.dump(d.unpackValue(v.Field(i))) + if i < (numFields - 1) { + d.w.Write(commaNewlineBytes) + } else { + d.w.Write(newlineBytes) + } + } + } + d.depth-- + d.indent() + d.w.Write(closeBraceBytes) + + case reflect.Uintptr: + printHexPtr(d.w, uintptr(v.Uint())) + + case reflect.UnsafePointer, reflect.Chan, reflect.Func: + printHexPtr(d.w, v.Pointer()) + + // There were not any other types at the time this code was written, but + // fall back to letting the default fmt package handle it in case any new + // types are added. + default: + if v.CanInterface() { + fmt.Fprintf(d.w, "%v", v.Interface()) + } else { + fmt.Fprintf(d.w, "%v", v.String()) + } + } +} + +// fdump is a helper function to consolidate the logic from the various public +// methods which take varying writers and config states. +func fdump(cs *ConfigState, w io.Writer, a ...interface{}) { + for _, arg := range a { + if arg == nil { + w.Write(interfaceBytes) + w.Write(spaceBytes) + w.Write(nilAngleBytes) + w.Write(newlineBytes) + continue + } + + d := dumpState{w: w, cs: cs} + d.pointers = make(map[uintptr]int) + d.dump(reflect.ValueOf(arg)) + d.w.Write(newlineBytes) + } +} + +// Fdump formats and displays the passed arguments to io.Writer w. It formats +// exactly the same as Dump. +func Fdump(w io.Writer, a ...interface{}) { + fdump(&Config, w, a...) +} + +// Sdump returns a string with the passed arguments formatted exactly the same +// as Dump. +func Sdump(a ...interface{}) string { + var buf bytes.Buffer + fdump(&Config, &buf, a...) + return buf.String() +} + +/* +Dump displays the passed parameters to standard out with newlines, customizable +indentation, and additional debug information such as complete types and all +pointer addresses used to indirect to the final value. It provides the +following features over the built-in printing facilities provided by the fmt +package: + + * Pointers are dereferenced and followed + * Circular data structures are detected and handled properly + * Custom Stringer/error interfaces are optionally invoked, including + on unexported types + * Custom types which only implement the Stringer/error interfaces via + a pointer receiver are optionally invoked when passing non-pointer + variables + * Byte arrays and slices are dumped like the hexdump -C command which + includes offsets, byte values in hex, and ASCII output + +The configuration options are controlled by an exported package global, +spew.Config. See ConfigState for options documentation. + +See Fdump if you would prefer dumping to an arbitrary io.Writer or Sdump to +get the formatted result as a string. +*/ +func Dump(a ...interface{}) { + fdump(&Config, os.Stdout, a...) +} diff --git a/vendor/github.com/davecgh/go-spew/spew/format.go b/vendor/github.com/davecgh/go-spew/spew/format.go new file mode 100644 index 0000000..b04edb7 --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/spew/format.go @@ -0,0 +1,419 @@ +/* + * Copyright (c) 2013-2016 Dave Collins + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package spew + +import ( + "bytes" + "fmt" + "reflect" + "strconv" + "strings" +) + +// supportedFlags is a list of all the character flags supported by fmt package. +const supportedFlags = "0-+# " + +// formatState implements the fmt.Formatter interface and contains information +// about the state of a formatting operation. The NewFormatter function can +// be used to get a new Formatter which can be used directly as arguments +// in standard fmt package printing calls. +type formatState struct { + value interface{} + fs fmt.State + depth int + pointers map[uintptr]int + ignoreNextType bool + cs *ConfigState +} + +// buildDefaultFormat recreates the original format string without precision +// and width information to pass in to fmt.Sprintf in the case of an +// unrecognized type. Unless new types are added to the language, this +// function won't ever be called. +func (f *formatState) buildDefaultFormat() (format string) { + buf := bytes.NewBuffer(percentBytes) + + for _, flag := range supportedFlags { + if f.fs.Flag(int(flag)) { + buf.WriteRune(flag) + } + } + + buf.WriteRune('v') + + format = buf.String() + return format +} + +// constructOrigFormat recreates the original format string including precision +// and width information to pass along to the standard fmt package. This allows +// automatic deferral of all format strings this package doesn't support. +func (f *formatState) constructOrigFormat(verb rune) (format string) { + buf := bytes.NewBuffer(percentBytes) + + for _, flag := range supportedFlags { + if f.fs.Flag(int(flag)) { + buf.WriteRune(flag) + } + } + + if width, ok := f.fs.Width(); ok { + buf.WriteString(strconv.Itoa(width)) + } + + if precision, ok := f.fs.Precision(); ok { + buf.Write(precisionBytes) + buf.WriteString(strconv.Itoa(precision)) + } + + buf.WriteRune(verb) + + format = buf.String() + return format +} + +// unpackValue returns values inside of non-nil interfaces when possible and +// ensures that types for values which have been unpacked from an interface +// are displayed when the show types flag is also set. +// This is useful for data types like structs, arrays, slices, and maps which +// can contain varying types packed inside an interface. +func (f *formatState) unpackValue(v reflect.Value) reflect.Value { + if v.Kind() == reflect.Interface { + f.ignoreNextType = false + if !v.IsNil() { + v = v.Elem() + } + } + return v +} + +// formatPtr handles formatting of pointers by indirecting them as necessary. +func (f *formatState) formatPtr(v reflect.Value) { + // Display nil if top level pointer is nil. + showTypes := f.fs.Flag('#') + if v.IsNil() && (!showTypes || f.ignoreNextType) { + f.fs.Write(nilAngleBytes) + return + } + + // Remove pointers at or below the current depth from map used to detect + // circular refs. + for k, depth := range f.pointers { + if depth >= f.depth { + delete(f.pointers, k) + } + } + + // Keep list of all dereferenced pointers to possibly show later. + pointerChain := make([]uintptr, 0) + + // Figure out how many levels of indirection there are by derferencing + // pointers and unpacking interfaces down the chain while detecting circular + // references. + nilFound := false + cycleFound := false + indirects := 0 + ve := v + for ve.Kind() == reflect.Ptr { + if ve.IsNil() { + nilFound = true + break + } + indirects++ + addr := ve.Pointer() + pointerChain = append(pointerChain, addr) + if pd, ok := f.pointers[addr]; ok && pd < f.depth { + cycleFound = true + indirects-- + break + } + f.pointers[addr] = f.depth + + ve = ve.Elem() + if ve.Kind() == reflect.Interface { + if ve.IsNil() { + nilFound = true + break + } + ve = ve.Elem() + } + } + + // Display type or indirection level depending on flags. + if showTypes && !f.ignoreNextType { + f.fs.Write(openParenBytes) + f.fs.Write(bytes.Repeat(asteriskBytes, indirects)) + f.fs.Write([]byte(ve.Type().String())) + f.fs.Write(closeParenBytes) + } else { + if nilFound || cycleFound { + indirects += strings.Count(ve.Type().String(), "*") + } + f.fs.Write(openAngleBytes) + f.fs.Write([]byte(strings.Repeat("*", indirects))) + f.fs.Write(closeAngleBytes) + } + + // Display pointer information depending on flags. + if f.fs.Flag('+') && (len(pointerChain) > 0) { + f.fs.Write(openParenBytes) + for i, addr := range pointerChain { + if i > 0 { + f.fs.Write(pointerChainBytes) + } + printHexPtr(f.fs, addr) + } + f.fs.Write(closeParenBytes) + } + + // Display dereferenced value. + switch { + case nilFound: + f.fs.Write(nilAngleBytes) + + case cycleFound: + f.fs.Write(circularShortBytes) + + default: + f.ignoreNextType = true + f.format(ve) + } +} + +// format is the main workhorse for providing the Formatter interface. It +// uses the passed reflect value to figure out what kind of object we are +// dealing with and formats it appropriately. It is a recursive function, +// however circular data structures are detected and handled properly. +func (f *formatState) format(v reflect.Value) { + // Handle invalid reflect values immediately. + kind := v.Kind() + if kind == reflect.Invalid { + f.fs.Write(invalidAngleBytes) + return + } + + // Handle pointers specially. + if kind == reflect.Ptr { + f.formatPtr(v) + return + } + + // Print type information unless already handled elsewhere. + if !f.ignoreNextType && f.fs.Flag('#') { + f.fs.Write(openParenBytes) + f.fs.Write([]byte(v.Type().String())) + f.fs.Write(closeParenBytes) + } + f.ignoreNextType = false + + // Call Stringer/error interfaces if they exist and the handle methods + // flag is enabled. + if !f.cs.DisableMethods { + if (kind != reflect.Invalid) && (kind != reflect.Interface) { + if handled := handleMethods(f.cs, f.fs, v); handled { + return + } + } + } + + switch kind { + case reflect.Invalid: + // Do nothing. We should never get here since invalid has already + // been handled above. + + case reflect.Bool: + printBool(f.fs, v.Bool()) + + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: + printInt(f.fs, v.Int(), 10) + + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: + printUint(f.fs, v.Uint(), 10) + + case reflect.Float32: + printFloat(f.fs, v.Float(), 32) + + case reflect.Float64: + printFloat(f.fs, v.Float(), 64) + + case reflect.Complex64: + printComplex(f.fs, v.Complex(), 32) + + case reflect.Complex128: + printComplex(f.fs, v.Complex(), 64) + + case reflect.Slice: + if v.IsNil() { + f.fs.Write(nilAngleBytes) + break + } + fallthrough + + case reflect.Array: + f.fs.Write(openBracketBytes) + f.depth++ + if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) { + f.fs.Write(maxShortBytes) + } else { + numEntries := v.Len() + for i := 0; i < numEntries; i++ { + if i > 0 { + f.fs.Write(spaceBytes) + } + f.ignoreNextType = true + f.format(f.unpackValue(v.Index(i))) + } + } + f.depth-- + f.fs.Write(closeBracketBytes) + + case reflect.String: + f.fs.Write([]byte(v.String())) + + case reflect.Interface: + // The only time we should get here is for nil interfaces due to + // unpackValue calls. + if v.IsNil() { + f.fs.Write(nilAngleBytes) + } + + case reflect.Ptr: + // Do nothing. We should never get here since pointers have already + // been handled above. + + case reflect.Map: + // nil maps should be indicated as different than empty maps + if v.IsNil() { + f.fs.Write(nilAngleBytes) + break + } + + f.fs.Write(openMapBytes) + f.depth++ + if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) { + f.fs.Write(maxShortBytes) + } else { + keys := v.MapKeys() + if f.cs.SortKeys { + sortValues(keys, f.cs) + } + for i, key := range keys { + if i > 0 { + f.fs.Write(spaceBytes) + } + f.ignoreNextType = true + f.format(f.unpackValue(key)) + f.fs.Write(colonBytes) + f.ignoreNextType = true + f.format(f.unpackValue(v.MapIndex(key))) + } + } + f.depth-- + f.fs.Write(closeMapBytes) + + case reflect.Struct: + numFields := v.NumField() + f.fs.Write(openBraceBytes) + f.depth++ + if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) { + f.fs.Write(maxShortBytes) + } else { + vt := v.Type() + for i := 0; i < numFields; i++ { + if i > 0 { + f.fs.Write(spaceBytes) + } + vtf := vt.Field(i) + if f.fs.Flag('+') || f.fs.Flag('#') { + f.fs.Write([]byte(vtf.Name)) + f.fs.Write(colonBytes) + } + f.format(f.unpackValue(v.Field(i))) + } + } + f.depth-- + f.fs.Write(closeBraceBytes) + + case reflect.Uintptr: + printHexPtr(f.fs, uintptr(v.Uint())) + + case reflect.UnsafePointer, reflect.Chan, reflect.Func: + printHexPtr(f.fs, v.Pointer()) + + // There were not any other types at the time this code was written, but + // fall back to letting the default fmt package handle it if any get added. + default: + format := f.buildDefaultFormat() + if v.CanInterface() { + fmt.Fprintf(f.fs, format, v.Interface()) + } else { + fmt.Fprintf(f.fs, format, v.String()) + } + } +} + +// Format satisfies the fmt.Formatter interface. See NewFormatter for usage +// details. +func (f *formatState) Format(fs fmt.State, verb rune) { + f.fs = fs + + // Use standard formatting for verbs that are not v. + if verb != 'v' { + format := f.constructOrigFormat(verb) + fmt.Fprintf(fs, format, f.value) + return + } + + if f.value == nil { + if fs.Flag('#') { + fs.Write(interfaceBytes) + } + fs.Write(nilAngleBytes) + return + } + + f.format(reflect.ValueOf(f.value)) +} + +// newFormatter is a helper function to consolidate the logic from the various +// public methods which take varying config states. +func newFormatter(cs *ConfigState, v interface{}) fmt.Formatter { + fs := &formatState{value: v, cs: cs} + fs.pointers = make(map[uintptr]int) + return fs +} + +/* +NewFormatter returns a custom formatter that satisfies the fmt.Formatter +interface. As a result, it integrates cleanly with standard fmt package +printing functions. The formatter is useful for inline printing of smaller data +types similar to the standard %v format specifier. + +The custom formatter only responds to the %v (most compact), %+v (adds pointer +addresses), %#v (adds types), or %#+v (adds types and pointer addresses) verb +combinations. Any other verbs such as %x and %q will be sent to the the +standard fmt package for formatting. In addition, the custom formatter ignores +the width and precision arguments (however they will still work on the format +specifiers not handled by the custom formatter). + +Typically this function shouldn't be called directly. It is much easier to make +use of the custom formatter by calling one of the convenience functions such as +Printf, Println, or Fprintf. +*/ +func NewFormatter(v interface{}) fmt.Formatter { + return newFormatter(&Config, v) +} diff --git a/vendor/github.com/davecgh/go-spew/spew/spew.go b/vendor/github.com/davecgh/go-spew/spew/spew.go new file mode 100644 index 0000000..32c0e33 --- /dev/null +++ b/vendor/github.com/davecgh/go-spew/spew/spew.go @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2013-2016 Dave Collins + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package spew + +import ( + "fmt" + "io" +) + +// Errorf is a wrapper for fmt.Errorf that treats each argument as if it were +// passed with a default Formatter interface returned by NewFormatter. It +// returns the formatted string as a value that satisfies error. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Errorf(format, spew.NewFormatter(a), spew.NewFormatter(b)) +func Errorf(format string, a ...interface{}) (err error) { + return fmt.Errorf(format, convertArgs(a)...) +} + +// Fprint is a wrapper for fmt.Fprint that treats each argument as if it were +// passed with a default Formatter interface returned by NewFormatter. It +// returns the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Fprint(w, spew.NewFormatter(a), spew.NewFormatter(b)) +func Fprint(w io.Writer, a ...interface{}) (n int, err error) { + return fmt.Fprint(w, convertArgs(a)...) +} + +// Fprintf is a wrapper for fmt.Fprintf that treats each argument as if it were +// passed with a default Formatter interface returned by NewFormatter. It +// returns the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Fprintf(w, format, spew.NewFormatter(a), spew.NewFormatter(b)) +func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) { + return fmt.Fprintf(w, format, convertArgs(a)...) +} + +// Fprintln is a wrapper for fmt.Fprintln that treats each argument as if it +// passed with a default Formatter interface returned by NewFormatter. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Fprintln(w, spew.NewFormatter(a), spew.NewFormatter(b)) +func Fprintln(w io.Writer, a ...interface{}) (n int, err error) { + return fmt.Fprintln(w, convertArgs(a)...) +} + +// Print is a wrapper for fmt.Print that treats each argument as if it were +// passed with a default Formatter interface returned by NewFormatter. It +// returns the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Print(spew.NewFormatter(a), spew.NewFormatter(b)) +func Print(a ...interface{}) (n int, err error) { + return fmt.Print(convertArgs(a)...) +} + +// Printf is a wrapper for fmt.Printf that treats each argument as if it were +// passed with a default Formatter interface returned by NewFormatter. It +// returns the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Printf(format, spew.NewFormatter(a), spew.NewFormatter(b)) +func Printf(format string, a ...interface{}) (n int, err error) { + return fmt.Printf(format, convertArgs(a)...) +} + +// Println is a wrapper for fmt.Println that treats each argument as if it were +// passed with a default Formatter interface returned by NewFormatter. It +// returns the number of bytes written and any write error encountered. See +// NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Println(spew.NewFormatter(a), spew.NewFormatter(b)) +func Println(a ...interface{}) (n int, err error) { + return fmt.Println(convertArgs(a)...) +} + +// Sprint is a wrapper for fmt.Sprint that treats each argument as if it were +// passed with a default Formatter interface returned by NewFormatter. It +// returns the resulting string. See NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Sprint(spew.NewFormatter(a), spew.NewFormatter(b)) +func Sprint(a ...interface{}) string { + return fmt.Sprint(convertArgs(a)...) +} + +// Sprintf is a wrapper for fmt.Sprintf that treats each argument as if it were +// passed with a default Formatter interface returned by NewFormatter. It +// returns the resulting string. See NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Sprintf(format, spew.NewFormatter(a), spew.NewFormatter(b)) +func Sprintf(format string, a ...interface{}) string { + return fmt.Sprintf(format, convertArgs(a)...) +} + +// Sprintln is a wrapper for fmt.Sprintln that treats each argument as if it +// were passed with a default Formatter interface returned by NewFormatter. It +// returns the resulting string. See NewFormatter for formatting details. +// +// This function is shorthand for the following syntax: +// +// fmt.Sprintln(spew.NewFormatter(a), spew.NewFormatter(b)) +func Sprintln(a ...interface{}) string { + return fmt.Sprintln(convertArgs(a)...) +} + +// convertArgs accepts a slice of arguments and returns a slice of the same +// length with each argument converted to a default spew Formatter interface. +func convertArgs(args []interface{}) (formatters []interface{}) { + formatters = make([]interface{}, len(args)) + for index, arg := range args { + formatters[index] = NewFormatter(arg) + } + return formatters +} diff --git a/vendor/github.com/ebitengine/oto/v3/.clang-format b/vendor/github.com/ebitengine/oto/v3/.clang-format new file mode 100644 index 0000000..e1d7fc9 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/.clang-format @@ -0,0 +1 @@ +CommentPragmas: '^go:build' diff --git a/vendor/github.com/ebitengine/oto/v3/.gitattributes b/vendor/github.com/ebitengine/oto/v3/.gitattributes new file mode 100644 index 0000000..8d7d19c --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/.gitattributes @@ -0,0 +1 @@ +internal/oboe/** linguist-vendored \ No newline at end of file diff --git a/vendor/github.com/ebitengine/oto/v3/.gitignore b/vendor/github.com/ebitengine/oto/v3/.gitignore new file mode 100644 index 0000000..538c8c5 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +*~ diff --git a/vendor/github.com/ebitengine/oto/v3/LICENSE b/vendor/github.com/ebitengine/oto/v3/LICENSE new file mode 100644 index 0000000..8dada3e --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/ebitengine/oto/v3/README.md b/vendor/github.com/ebitengine/oto/v3/README.md new file mode 100644 index 0000000..cda6663 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/README.md @@ -0,0 +1,234 @@ +# Oto (v3) + +[![Go Reference](https://pkg.go.dev/badge/github.com/ebitengine/oto/v3.svg)](https://pkg.go.dev/github.com/ebitengine/oto/v3) +[![Build Status](https://github.com/ebitengine/oto/actions/workflows/test.yml/badge.svg)](https://github.com/ebitengine/oto/actions?query=workflow%3Atest) + +A low-level library to play sound. + +- [Oto (v3)](#oto-v3) + - [Platforms](#platforms) + - [Prerequisite](#prerequisite) + - [macOS](#macos) + - [iOS](#ios) + - [Linux](#linux) + - [FreeBSD, OpenBSD](#freebsd-openbsd) + - [Usage](#usage) + - [Playing sounds from memory](#playing-sounds-from-memory) + - [Playing sounds by file streaming](#playing-sounds-by-file-streaming) + - [Advanced usage](#advanced-usage) + - [Crosscompiling](#crosscompiling) + +## Platforms + +- Windows (no Cgo required!) +- macOS (no Cgo required!) +- Linux +- FreeBSD +- OpenBSD +- Android +- iOS +- WebAssembly +- Nintendo Switch +- Xbox + +## Prerequisite + +On some platforms you will need a C/C++ compiler in your path that Go can use. + +- iOS: On newer macOS versions type `clang` on your terminal and a dialog with installation instructions will appear if you don't have it + - If you get an error with clang use xcode instead `xcode-select --install` +- Linux and other Unix systems: Should be installed by default, but if not try [GCC](https://gcc.gnu.org/) or [Clang](https://releases.llvm.org/download.html) + +### macOS + +Oto requires `AudioToolbox.framework`, but this is automatically linked. + +### iOS + +Oto requires these frameworks: + +- `AVFoundation.framework` +- `AudioToolbox.framework` + +Add them to "Linked Frameworks and Libraries" on your Xcode project. + +### Linux + +ALSA is required. On Ubuntu or Debian, run this command: + +```sh +apt install libasound2-dev +``` + +On RedHat-based linux distributions, run: + +```sh +dnf install alsa-lib-devel +``` + +In most cases this command must be run by root user or through `sudo` command. + +### FreeBSD, OpenBSD + +BSD systems are not tested well. If ALSA works, Oto should work. + +## Usage + +The two main components of Oto are a `Context` and `Players`. The context handles interactions with +the OS and audio drivers, and as such there can only be **one** context in your program. + +From a context you can create any number of different players, where each player is given an `io.Reader` that +it reads bytes representing sounds from and plays. + +Note that a single `io.Reader` must **not** be used by multiple players. + +### Playing sounds from memory + +The following is an example of loading and playing an MP3 file: + +```go +package main + +import ( + "bytes" + "time" + "os" + + "github.com/ebitengine/oto/v3" + "github.com/hajimehoshi/go-mp3" +) + +func main() { + // Read the mp3 file into memory + fileBytes, err := os.ReadFile("./my-file.mp3") + if err != nil { + panic("reading my-file.mp3 failed: " + err.Error()) + } + + // Convert the pure bytes into a reader object that can be used with the mp3 decoder + fileBytesReader := bytes.NewReader(fileBytes) + + // Decode file + decodedMp3, err := mp3.NewDecoder(fileBytesReader) + if err != nil { + panic("mp3.NewDecoder failed: " + err.Error()) + } + + // Prepare an Oto context (this will use your default audio device) that will + // play all our sounds. Its configuration can't be changed later. + + op := &oto.NewContextOptions{} + + // Usually 44100 or 48000. Other values might cause distortions in Oto + op.SampleRate = 44100 + + // Number of channels (aka locations) to play sounds from. Either 1 or 2. + // 1 is mono sound, and 2 is stereo (most speakers are stereo). + op.ChannelCount = 2 + + // Format of the source. go-mp3's format is signed 16bit integers. + op.Format = oto.FormatSignedInt16LE + + // Remember that you should **not** create more than one context + otoCtx, readyChan, err := oto.NewContext(op) + if err != nil { + panic("oto.NewContext failed: " + err.Error()) + } + // It might take a bit for the hardware audio devices to be ready, so we wait on the channel. + <-readyChan + + // Create a new 'player' that will handle our sound. Paused by default. + player := otoCtx.NewPlayer(decodedMp3) + + // Play starts playing the sound and returns without waiting for it (Play() is async). + player.Play() + + // We can wait for the sound to finish playing using something like this + for player.IsPlaying() { + time.Sleep(time.Millisecond) + } + + // Now that the sound finished playing, we can restart from the beginning (or go to any location in the sound) using seek + // newPos, err := player.(io.Seeker).Seek(0, io.SeekStart) + // if err != nil{ + // panic("player.Seek failed: " + err.Error()) + // } + // println("Player is now at position:", newPos) + // player.Play() + + // If you don't want the player/sound anymore simply close + err = player.Close() + if err != nil { + panic("player.Close failed: " + err.Error()) + } +} +``` + +### Playing sounds by file streaming + +The above example loads the entire file into memory and then plays it. This is great for smaller files +but might be an issue if you are playing a long song since it would take too much memory and too long to load. + +In such cases you might want to stream the file. Luckily this is very simple, just use `os.Open`: + +```go +package main + +import ( + "os" + "time" + + "github.com/ebitengine/oto/v3" + "github.com/hajimehoshi/go-mp3" +) + +func main() { + // Open the file for reading. Do NOT close before you finish playing! + file, err := os.Open("./my-file.mp3") + if err != nil { + panic("opening my-file.mp3 failed: " + err.Error()) + } + + // Decode file. This process is done as the file plays so it won't + // load the whole thing into memory. + decodedMp3, err := mp3.NewDecoder(file) + if err != nil { + panic("mp3.NewDecoder failed: " + err.Error()) + } + + // Rest is the same... + + // Close file only after you finish playing + file.Close() +} +``` + +The only thing to note about streaming is that the *file* object must be kept alive, otherwise +you might just play static. + +To keep it alive not only must you be careful about when you close it, but you might need to keep a reference +to the original file object alive (by for example keeping it in a struct). + +### Advanced usage + +Players have their own internal audio data buffer, so while for example 200 bytes have been read from the `io.Reader` that +doesn't mean they were all played from the audio device. + +Data is moved from io.Reader->internal buffer->audio device, and when the internal buffer moves data to the audio device +is not guaranteed, so there might be a small delay. The amount of data in the buffer can be retrieved +using `Player.UnplayedBufferSize()`. + +The size of the underlying buffer of a player can also be set by type-asserting the player object: + +```go +myPlayer.(oto.BufferSizeSetter).SetBufferSize(newBufferSize) +``` + +This works because players implement a `Player` interface and a `BufferSizeSetter` interface. + +## Crosscompiling + +Crosscompiling to macOS or Windows is as easy as setting `GOOS=darwin` or `GOOS=windows`, respectively. + +To crosscompile for other platforms, make sure the libraries for the target architecture are installed, and set +`CGO_ENABLED=1` as Go disables [Cgo](https://golang.org/cmd/cgo/#hdr-Using_cgo_with_the_go_command) on crosscompiles by default. diff --git a/vendor/github.com/ebitengine/oto/v3/api_darwin.go b/vendor/github.com/ebitengine/oto/v3/api_darwin.go new file mode 100644 index 0000000..795bf84 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/api_darwin.go @@ -0,0 +1,95 @@ +// Copyright 2022 The Oto Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package oto + +import ( + "unsafe" + + "github.com/ebitengine/purego" +) + +const ( + avAudioSessionErrorCodeCannotStartPlaying = 0x21706c61 // '!pla' + avAudioSessionErrorCodeCannotInterruptOthers = 0x21696e74 // '!int' + avAudioSessionErrorCodeSiriIsRecording = 0x73697269 // 'siri' +) + +const ( + kAudioFormatLinearPCM = 0x6C70636D //'lpcm' +) + +const ( + kAudioFormatFlagIsFloat = 1 << 0 // 0x1 +) + +type _AudioStreamBasicDescription struct { + mSampleRate float64 + mFormatID uint32 + mFormatFlags uint32 + mBytesPerPacket uint32 + mFramesPerPacket uint32 + mBytesPerFrame uint32 + mChannelsPerFrame uint32 + mBitsPerChannel uint32 + mReserved uint32 +} + +type _AudioQueueRef uintptr + +type _AudioTimeStamp uintptr + +type _AudioStreamPacketDescription struct { + mStartOffset int64 + mVariableFramesInPacket uint32 + mDataByteSize uint32 +} + +type _AudioQueueBufferRef *_AudioQueueBuffer + +type _AudioQueueBuffer struct { + mAudioDataBytesCapacity uint32 + mAudioData uintptr // void* + mAudioDataByteSize uint32 + mUserData uintptr // void* + + mPacketDescriptionCapacity uint32 + mPacketDescriptions *_AudioStreamPacketDescription + mPacketDescriptionCount uint32 +} + +type _AudioQueueOutputCallback func(inUserData unsafe.Pointer, inAQ _AudioQueueRef, inBuffer _AudioQueueBufferRef) + +func initializeAPI() error { + toolbox, err := purego.Dlopen("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox", purego.RTLD_LAZY|purego.RTLD_GLOBAL) + if err != nil { + return err + } + purego.RegisterLibFunc(&_AudioQueueNewOutput, toolbox, "AudioQueueNewOutput") + purego.RegisterLibFunc(&_AudioQueueAllocateBuffer, toolbox, "AudioQueueAllocateBuffer") + purego.RegisterLibFunc(&_AudioQueueEnqueueBuffer, toolbox, "AudioQueueEnqueueBuffer") + purego.RegisterLibFunc(&_AudioQueueStart, toolbox, "AudioQueueStart") + purego.RegisterLibFunc(&_AudioQueuePause, toolbox, "AudioQueuePause") + return nil +} + +var _AudioQueueNewOutput func(inFormat *_AudioStreamBasicDescription, inCallbackProc _AudioQueueOutputCallback, inUserData unsafe.Pointer, inCallbackRunLoop uintptr, inCallbackRunLoopMod uintptr, inFlags uint32, outAQ *_AudioQueueRef) uintptr + +var _AudioQueueAllocateBuffer func(inAQ _AudioQueueRef, inBufferByteSize uint32, outBuffer *_AudioQueueBufferRef) uintptr + +var _AudioQueueEnqueueBuffer func(inAQ _AudioQueueRef, inBuffer _AudioQueueBufferRef, inNumPacketDescs uint32, inPackets []_AudioStreamPacketDescription) uintptr + +var _AudioQueueStart func(inAQ _AudioQueueRef, inStartTime *_AudioTimeStamp) uintptr + +var _AudioQueuePause func(inAQ _AudioQueueRef) uintptr diff --git a/vendor/github.com/ebitengine/oto/v3/api_wasapi_windows.go b/vendor/github.com/ebitengine/oto/v3/api_wasapi_windows.go new file mode 100644 index 0000000..b5a051a --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/api_wasapi_windows.go @@ -0,0 +1,479 @@ +// Copyright 2022 The Oto Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package oto + +import ( + "fmt" + "runtime" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +var ( + ole32 = windows.NewLazySystemDLL("ole32") +) + +var ( + procCoCreateInstance = ole32.NewProc("CoCreateInstance") +) + +type _REFERENCE_TIME int64 + +var ( + uuidIAudioClient2 = windows.GUID{0x726778cd, 0xf60a, 0x4eda, [...]byte{0x82, 0xde, 0xe4, 0x76, 0x10, 0xcd, 0x78, 0xaa}} + uuidIAudioRenderClient = windows.GUID{0xf294acfc, 0x3146, 0x4483, [...]byte{0xa7, 0xbf, 0xad, 0xdc, 0xa7, 0xc2, 0x60, 0xe2}} + uuidIMMDeviceEnumerator = windows.GUID{0xa95664d2, 0x9614, 0x4f35, [...]byte{0xa7, 0x46, 0xde, 0x8d, 0xb6, 0x36, 0x17, 0xe6}} + uuidMMDeviceEnumerator = windows.GUID{0xbcde0395, 0xe52f, 0x467c, [...]byte{0x8e, 0x3d, 0xc4, 0x57, 0x92, 0x91, 0x69, 0x2e}} +) + +const ( + _AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM = 0x80000000 + _AUDCLNT_STREAMFLAGS_EVENTCALLBACK = 0x00040000 + _AUDCLNT_STREAMFLAGS_NOPERSIST = 0x00080000 + _COINIT_APARTMENTTHREADED = 0x2 + _COINIT_MULTITHREADED = 0 + _REFTIMES_PER_SEC = 10000000 + _SPEAKER_FRONT_CENTER = 0x4 + _SPEAKER_FRONT_LEFT = 0x1 + _SPEAKER_FRONT_RIGHT = 0x2 + _WAVE_FORMAT_EXTENSIBLE = 0xfffe +) + +var ( + _KSDATAFORMAT_SUBTYPE_IEEE_FLOAT = windows.GUID{0x00000003, 0x0000, 0x0010, [...]byte{0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}} + _KSDATAFORMAT_SUBTYPE_PCM = windows.GUID{0x00000001, 0x0000, 0x0010, [...]byte{0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}} +) + +type _AUDCLNT_ERR uint32 + +const ( + _AUDCLNT_E_DEVICE_INVALIDATED _AUDCLNT_ERR = 0x88890004 + _AUDCLNT_E_NOT_INITIALIZED _AUDCLNT_ERR = 0x88890001 + _AUDCLNT_E_RESOURCES_INVALIDATED _AUDCLNT_ERR = 0x88890026 +) + +func isAudclntErr(hresult uint32) bool { + return hresult&0xffff0000 == (1<<31)|(windows.FACILITY_AUDCLNT<<16) +} + +func (e _AUDCLNT_ERR) Error() string { + switch e { + case _AUDCLNT_E_DEVICE_INVALIDATED: + return "AUDCLNT_E_DEVICE_INVALIDATED" + case _AUDCLNT_E_RESOURCES_INVALIDATED: + return "AUDCLNT_E_RESOURCES_INVALIDATED" + default: + return fmt.Sprintf("AUDCLNT_ERR(%d)", e) + } +} + +type _AUDCLNT_SHAREMODE int32 + +const ( + _AUDCLNT_SHAREMODE_SHARED _AUDCLNT_SHAREMODE = 0 + _AUDCLNT_SHAREMODE_EXCLUSIVE _AUDCLNT_SHAREMODE = 1 +) + +type _AUDCLNT_STREAMOPTIONS int32 + +const ( + _AUDCLNT_STREAMOPTIONS_NONE _AUDCLNT_STREAMOPTIONS = 0x0 + _AUDCLNT_STREAMOPTIONS_RAW _AUDCLNT_STREAMOPTIONS = 0x1 + _AUDCLNT_STREAMOPTIONS_MATCH_FORMAT _AUDCLNT_STREAMOPTIONS = 0x2 + _AUDCLNT_STREAMOPTIONS_AMBISONICS _AUDCLNT_STREAMOPTIONS = 0x4 +) + +type _AUDIO_STREAM_CATEGORY int32 + +const ( + _AudioCategory_Other _AUDIO_STREAM_CATEGORY = 0 + _AudioCategory_ForegroundOnlyMedia _AUDIO_STREAM_CATEGORY = 1 + _AudioCategory_BackgroundCapableMedia _AUDIO_STREAM_CATEGORY = 2 + _AudioCategory_Communications _AUDIO_STREAM_CATEGORY = 3 + _AudioCategory_Alerts _AUDIO_STREAM_CATEGORY = 4 + _AudioCategory_SoundEffects _AUDIO_STREAM_CATEGORY = 5 + _AudioCategory_GameEffects _AUDIO_STREAM_CATEGORY = 6 + _AudioCategory_GameMedia _AUDIO_STREAM_CATEGORY = 7 + _AudioCategory_GameChat _AUDIO_STREAM_CATEGORY = 8 + _AudioCategory_Speech _AUDIO_STREAM_CATEGORY = 9 + _AudioCategory_Movie _AUDIO_STREAM_CATEGORY = 10 + _AudioCategory_Media _AUDIO_STREAM_CATEGORY = 11 +) + +type _CLSCTX int32 + +const ( + _CLSCTX_INPROC_SERVER _CLSCTX = 0x00000001 + _CLSCTX_INPROC_HANDLER _CLSCTX = 0x00000002 + _CLSCTX_LOCAL_SERVER _CLSCTX = 0x00000004 + _CLSCTX_REMOTE_SERVER _CLSCTX = 0x00000010 + _CLSCTX_ALL = _CLSCTX_INPROC_SERVER | _CLSCTX_INPROC_HANDLER | _CLSCTX_LOCAL_SERVER | _CLSCTX_REMOTE_SERVER +) + +type _EDataFlow int32 + +const ( + eRender _EDataFlow = 0 +) + +type _ERole int32 + +const ( + eConsole _ERole = 0 +) + +type _WIN32_ERR uint32 + +const ( + _E_NOTFOUND _WIN32_ERR = 0x80070490 +) + +func isWin32Err(hresult uint32) bool { + return hresult&0xffff0000 == (1<<31)|(windows.FACILITY_WIN32<<16) +} + +func (e _WIN32_ERR) Error() string { + switch e { + case _E_NOTFOUND: + return "E_NOTFOUND" + default: + return fmt.Sprintf("HRESULT(%d)", e) + } +} + +type _AudioClientProperties struct { + cbSize uint32 + bIsOffload int32 + eCategory _AUDIO_STREAM_CATEGORY + Options _AUDCLNT_STREAMOPTIONS +} + +type _PROPVARIANT struct { + // TODO: Implmeent this +} + +type _WAVEFORMATEXTENSIBLE struct { + wFormatTag uint16 + nChannels uint16 + nSamplesPerSec uint32 + nAvgBytesPerSec uint32 + nBlockAlign uint16 + wBitsPerSample uint16 + cbSize uint16 + Samples uint16 // union + dwChannelMask uint32 + SubFormat windows.GUID +} + +func _CoCreateInstance(rclsid *windows.GUID, pUnkOuter unsafe.Pointer, dwClsContext uint32, riid *windows.GUID) (unsafe.Pointer, error) { + var v unsafe.Pointer + r, _, _ := procCoCreateInstance.Call(uintptr(unsafe.Pointer(rclsid)), uintptr(pUnkOuter), uintptr(dwClsContext), uintptr(unsafe.Pointer(riid)), uintptr(unsafe.Pointer(&v))) + runtime.KeepAlive(rclsid) + runtime.KeepAlive(riid) + if uint32(r) != uint32(windows.S_OK) { + return nil, fmt.Errorf("oto: CoCreateInstance failed: HRESULT(%d)", uint32(r)) + } + return v, nil +} + +type _IAudioClient2 struct { + vtbl *_IAudioClient2_Vtbl +} + +type _IAudioClient2_Vtbl struct { + QueryInterface uintptr + AddRef uintptr + Release uintptr + + Initialize uintptr + GetBufferSize uintptr + GetStreamLatency uintptr + GetCurrentPadding uintptr + IsFormatSupported uintptr + GetMixFormat uintptr + GetDevicePeriod uintptr + Start uintptr + Stop uintptr + Reset uintptr + SetEventHandle uintptr + GetService uintptr + IsOffloadCapable uintptr + SetClientProperties uintptr + GetBufferSizeLimits uintptr +} + +func (i *_IAudioClient2) GetBufferSize() (uint32, error) { + var numBufferFrames uint32 + r, _, _ := syscall.Syscall(i.vtbl.GetBufferSize, 2, uintptr(unsafe.Pointer(i)), uintptr(unsafe.Pointer(&numBufferFrames)), 0) + if uint32(r) != uint32(windows.S_OK) { + if isAudclntErr(uint32(r)) { + return 0, fmt.Errorf("oto: IAudioClient2::GetBufferSize failed: %w", _AUDCLNT_ERR(r)) + } + return 0, fmt.Errorf("oto: IAudioClient2::GetBufferSize failed: HRESULT(%d)", uint32(r)) + } + return numBufferFrames, nil +} + +func (i *_IAudioClient2) GetCurrentPadding() (uint32, error) { + var numPaddingFrames uint32 + r, _, _ := syscall.Syscall(i.vtbl.GetCurrentPadding, 2, uintptr(unsafe.Pointer(i)), uintptr(unsafe.Pointer(&numPaddingFrames)), 0) + if uint32(r) != uint32(windows.S_OK) { + if isAudclntErr(uint32(r)) { + return 0, fmt.Errorf("oto: IAudioClient2::GetCurrentPadding failed: %w", _AUDCLNT_ERR(r)) + } + return 0, fmt.Errorf("oto: IAudioClient2::GetCurrentPadding failed: HRESULT(%d)", uint32(r)) + } + return numPaddingFrames, nil +} + +func (i *_IAudioClient2) GetDevicePeriod() (_REFERENCE_TIME, _REFERENCE_TIME, error) { + var defaultDevicePeriod _REFERENCE_TIME + var minimumDevicePeriod _REFERENCE_TIME + r, _, _ := syscall.Syscall(i.vtbl.GetDevicePeriod, 3, uintptr(unsafe.Pointer(i)), + uintptr(unsafe.Pointer(&defaultDevicePeriod)), uintptr(unsafe.Pointer(&minimumDevicePeriod))) + if uint32(r) != uint32(windows.S_OK) { + if isAudclntErr(uint32(r)) { + return 0, 0, fmt.Errorf("oto: IAudioClient2::GetDevicePeriod failed: %w", _AUDCLNT_ERR(r)) + } + return 0, 0, fmt.Errorf("oto: IAudioClient2::GetDevicePeriod failed: HRESULT(%d)", uint32(r)) + } + return defaultDevicePeriod, minimumDevicePeriod, nil +} + +func (i *_IAudioClient2) GetService(riid *windows.GUID) (unsafe.Pointer, error) { + var v unsafe.Pointer + r, _, _ := syscall.Syscall(i.vtbl.GetService, 3, uintptr(unsafe.Pointer(i)), uintptr(unsafe.Pointer(riid)), uintptr(unsafe.Pointer(&v))) + if uint32(r) != uint32(windows.S_OK) { + if isAudclntErr(uint32(r)) { + return nil, fmt.Errorf("oto: IAudioClient2::GetService failed: %w", _AUDCLNT_ERR(r)) + } + return nil, fmt.Errorf("oto: IAudioClient2::GetService failed: HRESULT(%d)", uint32(r)) + } + return v, nil +} + +func (i *_IAudioClient2) Initialize(shareMode _AUDCLNT_SHAREMODE, streamFlags uint32, hnsBufferDuration _REFERENCE_TIME, hnsPeriodicity _REFERENCE_TIME, pFormat *_WAVEFORMATEXTENSIBLE, audioSessionGuid *windows.GUID) error { + var r uintptr + if unsafe.Sizeof(uintptr(0)) == 8 { + // 64bits + r, _, _ = syscall.Syscall9(i.vtbl.Initialize, 7, uintptr(unsafe.Pointer(i)), + uintptr(shareMode), uintptr(streamFlags), uintptr(hnsBufferDuration), + uintptr(hnsPeriodicity), uintptr(unsafe.Pointer(pFormat)), uintptr(unsafe.Pointer(audioSessionGuid)), + 0, 0) + } else { + // 32bits + r, _, _ = syscall.Syscall9(i.vtbl.Initialize, 9, uintptr(unsafe.Pointer(i)), + uintptr(shareMode), uintptr(streamFlags), uintptr(hnsBufferDuration), + uintptr(hnsBufferDuration>>32), uintptr(hnsPeriodicity), uintptr(hnsPeriodicity>>32), + uintptr(unsafe.Pointer(pFormat)), uintptr(unsafe.Pointer(audioSessionGuid))) + } + runtime.KeepAlive(pFormat) + runtime.KeepAlive(audioSessionGuid) + if uint32(r) != uint32(windows.S_OK) { + if isAudclntErr(uint32(r)) { + return fmt.Errorf("oto: IAudioClient2::Initialize failed: %w", _AUDCLNT_ERR(r)) + } + return fmt.Errorf("oto: IAudioClient2::Initialize failed: HRESULT(%d)", uint32(r)) + } + return nil +} + +func (i *_IAudioClient2) IsFormatSupported(shareMode _AUDCLNT_SHAREMODE, pFormat *_WAVEFORMATEXTENSIBLE) (*_WAVEFORMATEXTENSIBLE, error) { + var closestMatch *_WAVEFORMATEXTENSIBLE + r, _, _ := syscall.Syscall6(i.vtbl.IsFormatSupported, 4, uintptr(unsafe.Pointer(i)), + uintptr(shareMode), uintptr(unsafe.Pointer(pFormat)), uintptr(unsafe.Pointer(&closestMatch)), + 0, 0) + if uint32(r) != uint32(windows.S_OK) { + if uint32(r) == uint32(windows.S_FALSE) { + var r _WAVEFORMATEXTENSIBLE + if closestMatch != nil { + r = *closestMatch + windows.CoTaskMemFree(unsafe.Pointer(closestMatch)) + } + return &r, nil + } + if isAudclntErr(uint32(r)) { + return nil, fmt.Errorf("oto: IAudioClient2::IsFormatSupported failed: %w", _AUDCLNT_ERR(r)) + } + return nil, fmt.Errorf("oto: IAudioClient2::IsFormatSupported failed: HRESULT(%d)", uint32(r)) + } + return nil, nil +} + +func (i *_IAudioClient2) Release() uint32 { + r, _, _ := syscall.Syscall(i.vtbl.Release, 1, uintptr(unsafe.Pointer(i)), 0, 0) + return uint32(r) +} + +func (i *_IAudioClient2) SetClientProperties(pProperties *_AudioClientProperties) error { + r, _, _ := syscall.Syscall(i.vtbl.SetClientProperties, 2, uintptr(unsafe.Pointer(i)), uintptr(unsafe.Pointer(pProperties)), 0) + runtime.KeepAlive(pProperties) + if uint32(r) != uint32(windows.S_OK) { + if isAudclntErr(uint32(r)) { + return fmt.Errorf("oto: IAudioClient2::SetClientProperties failed: %w", _AUDCLNT_ERR(r)) + } + return fmt.Errorf("oto: IAudioClient2::SetClientProperties failed: HRESULT(%d)", uint32(r)) + } + return nil +} + +func (i *_IAudioClient2) SetEventHandle(eventHandle windows.Handle) error { + r, _, _ := syscall.Syscall(i.vtbl.SetEventHandle, 2, uintptr(unsafe.Pointer(i)), uintptr(eventHandle), 0) + if uint32(r) != uint32(windows.S_OK) { + if isAudclntErr(uint32(r)) { + return fmt.Errorf("oto: IAudioClient2::SetEventHandle failed: %w", _AUDCLNT_ERR(r)) + } + return fmt.Errorf("oto: IAudioClient2::SetEventHandle failed: HRESULT(%d)", uint32(r)) + } + return nil +} + +func (i *_IAudioClient2) Start() error { + r, _, _ := syscall.Syscall(i.vtbl.Start, 1, uintptr(unsafe.Pointer(i)), 0, 0) + if uint32(r) != uint32(windows.S_OK) { + if isAudclntErr(uint32(r)) { + return fmt.Errorf("oto: IAudioClient2::Start failed: %w", _AUDCLNT_ERR(r)) + } + return fmt.Errorf("oto: IAudioClient2::Start failed: HRESULT(%d)", uint32(r)) + } + return nil +} + +func (i *_IAudioClient2) Stop() (bool, error) { + r, _, _ := syscall.Syscall(i.vtbl.Stop, 1, uintptr(unsafe.Pointer(i)), 0, 0) + if uint32(r) != uint32(windows.S_OK) && uint32(r) != uint32(windows.S_FALSE) { + if isAudclntErr(uint32(r)) { + return false, fmt.Errorf("oto: IAudioClient2::Stop failed: %w", _AUDCLNT_ERR(r)) + } + return false, fmt.Errorf("oto: IAudioClient2::Stop failed: HRESULT(%d)", uint32(r)) + } + return uint32(r) == uint32(windows.S_OK), nil +} + +type _IAudioRenderClient struct { + vtbl *_IAudioRenderClient_Vtbl +} + +type _IAudioRenderClient_Vtbl struct { + QueryInterface uintptr + AddRef uintptr + Release uintptr + + GetBuffer uintptr + ReleaseBuffer uintptr +} + +func (i *_IAudioRenderClient) GetBuffer(numFramesRequested uint32) (*byte, error) { + var data *byte + r, _, _ := syscall.Syscall(i.vtbl.GetBuffer, 3, uintptr(unsafe.Pointer(i)), uintptr(numFramesRequested), uintptr(unsafe.Pointer(&data))) + if uint32(r) != uint32(windows.S_OK) { + if isAudclntErr(uint32(r)) { + return nil, fmt.Errorf("oto: IAudioRenderClient::GetBuffer failed: %w", _AUDCLNT_ERR(r)) + } + return nil, fmt.Errorf("oto: IAudioRenderClient::GetBuffer failed: HRESULT(%d)", uint32(r)) + } + return data, nil +} + +func (i *_IAudioRenderClient) Release() uint32 { + r, _, _ := syscall.Syscall(i.vtbl.Release, 1, uintptr(unsafe.Pointer(i)), 0, 0) + return uint32(r) +} + +func (i *_IAudioRenderClient) ReleaseBuffer(numFramesWritten uint32, dwFlags uint32) error { + r, _, _ := syscall.Syscall(i.vtbl.ReleaseBuffer, 3, uintptr(unsafe.Pointer(i)), uintptr(numFramesWritten), uintptr(dwFlags)) + if uint32(r) != uint32(windows.S_OK) { + if isAudclntErr(uint32(r)) { + return fmt.Errorf("oto: IAudioRenderClient::ReleaseBuffer failed: %w", _AUDCLNT_ERR(r)) + } + return fmt.Errorf("oto: IAudioRenderClient::ReleaseBuffer failed: HRESULT(%d)", uint32(r)) + } + return nil +} + +type _IMMDevice struct { + vtbl *_IMMDevice_Vtbl +} + +type _IMMDevice_Vtbl struct { + QueryInterface uintptr + AddRef uintptr + Release uintptr + + Activate uintptr + OpenPropertyStore uintptr + GetId uintptr + GetState uintptr +} + +func (i *_IMMDevice) Activate(iid *windows.GUID, dwClsCtx uint32, pActivationParams *_PROPVARIANT) (unsafe.Pointer, error) { + var v unsafe.Pointer + r, _, _ := syscall.Syscall6(i.vtbl.Activate, 5, uintptr(unsafe.Pointer(i)), + uintptr(unsafe.Pointer(iid)), uintptr(dwClsCtx), uintptr(unsafe.Pointer(pActivationParams)), uintptr(unsafe.Pointer(&v)), 0) + runtime.KeepAlive(iid) + runtime.KeepAlive(pActivationParams) + if uint32(r) != uint32(windows.S_OK) { + return nil, fmt.Errorf("oto: IMMDevice::Activate failed: HRESULT(%d)", uint32(r)) + } + return v, nil +} + +func (i *_IMMDevice) GetId() (string, error) { + var strId *uint16 + r, _, _ := syscall.Syscall(i.vtbl.GetId, 2, uintptr(unsafe.Pointer(i)), uintptr(unsafe.Pointer(&strId)), 0) + if uint32(r) != uint32(windows.S_OK) { + return "", fmt.Errorf("oto: IMMDevice::GetId failed: HRESULT(%d)", uint32(r)) + } + return windows.UTF16PtrToString(strId), nil +} + +func (i *_IMMDevice) Release() { + syscall.Syscall(i.vtbl.Release, 1, uintptr(unsafe.Pointer(i)), 0, 0) +} + +type _IMMDeviceEnumerator struct { + vtbl *_IMMDeviceEnumerator_Vtbl +} + +type _IMMDeviceEnumerator_Vtbl struct { + QueryInterface uintptr + AddRef uintptr + Release uintptr + + EnumAudioEndpoints uintptr + GetDefaultAudioEndpoint uintptr + GetDevice uintptr + RegisterEndpointNotificationCallback uintptr + UnregisterEndpointNotificationCallback uintptr +} + +func (i *_IMMDeviceEnumerator) GetDefaultAudioEndPoint(dataFlow _EDataFlow, role _ERole) (*_IMMDevice, error) { + var endPoint *_IMMDevice + r, _, _ := syscall.Syscall6(i.vtbl.GetDefaultAudioEndpoint, 4, uintptr(unsafe.Pointer(i)), + uintptr(dataFlow), uintptr(role), uintptr(unsafe.Pointer(&endPoint)), 0, 0) + if uint32(r) != uint32(windows.S_OK) { + if isWin32Err(uint32(r)) { + return nil, fmt.Errorf("oto: IMMDeviceEnumerator::GetDefaultAudioEndPoint failed: %w", _E_NOTFOUND) + } + return nil, fmt.Errorf("oto: IMMDeviceEnumerator::GetDefaultAudioEndPoint failed: HRESULT(%d)", uint32(r)) + } + return endPoint, nil +} + +func (i *_IMMDeviceEnumerator) Release() { + syscall.Syscall(i.vtbl.Release, 1, uintptr(unsafe.Pointer(i)), 0, 0) +} diff --git a/vendor/github.com/ebitengine/oto/v3/api_winmm_windows.go b/vendor/github.com/ebitengine/oto/v3/api_winmm_windows.go new file mode 100644 index 0000000..821299c --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/api_winmm_windows.go @@ -0,0 +1,174 @@ +// Copyright 2021 The Oto Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package oto + +import ( + "fmt" + "runtime" + "unsafe" + + "golang.org/x/sys/windows" +) + +var ( + winmm = windows.NewLazySystemDLL("winmm") +) + +var ( + procWaveOutOpen = winmm.NewProc("waveOutOpen") + procWaveOutClose = winmm.NewProc("waveOutClose") + procWaveOutPrepareHeader = winmm.NewProc("waveOutPrepareHeader") + procWaveOutUnprepareHeader = winmm.NewProc("waveOutUnprepareHeader") + procWaveOutWrite = winmm.NewProc("waveOutWrite") +) + +type _WAVEHDR struct { + lpData uintptr + dwBufferLength uint32 + dwBytesRecorded uint32 + dwUser uintptr + dwFlags uint32 + dwLoops uint32 + lpNext uintptr + reserved uintptr +} + +type _WAVEFORMATEX struct { + wFormatTag uint16 + nChannels uint16 + nSamplesPerSec uint32 + nAvgBytesPerSec uint32 + nBlockAlign uint16 + wBitsPerSample uint16 + cbSize uint16 +} + +const ( + _WAVE_FORMAT_IEEE_FLOAT = 3 + _WHDR_INQUEUE = 16 +) + +type _MMRESULT uint + +const ( + _MMSYSERR_NOERROR _MMRESULT = 0 + _MMSYSERR_ERROR _MMRESULT = 1 + _MMSYSERR_BADDEVICEID _MMRESULT = 2 + _MMSYSERR_ALLOCATED _MMRESULT = 4 + _MMSYSERR_INVALIDHANDLE _MMRESULT = 5 + _MMSYSERR_NODRIVER _MMRESULT = 6 + _MMSYSERR_NOMEM _MMRESULT = 7 + _WAVERR_BADFORMAT _MMRESULT = 32 + _WAVERR_STILLPLAYING _MMRESULT = 33 + _WAVERR_UNPREPARED _MMRESULT = 34 + _WAVERR_SYNC _MMRESULT = 35 +) + +func (m _MMRESULT) Error() string { + switch m { + case _MMSYSERR_NOERROR: + return "MMSYSERR_NOERROR" + case _MMSYSERR_ERROR: + return "MMSYSERR_ERROR" + case _MMSYSERR_BADDEVICEID: + return "MMSYSERR_BADDEVICEID" + case _MMSYSERR_ALLOCATED: + return "MMSYSERR_ALLOCATED" + case _MMSYSERR_INVALIDHANDLE: + return "MMSYSERR_INVALIDHANDLE" + case _MMSYSERR_NODRIVER: + return "MMSYSERR_NODRIVER" + case _MMSYSERR_NOMEM: + return "MMSYSERR_NOMEM" + case _WAVERR_BADFORMAT: + return "WAVERR_BADFORMAT" + case _WAVERR_STILLPLAYING: + return "WAVERR_STILLPLAYING" + case _WAVERR_UNPREPARED: + return "WAVERR_UNPREPARED" + case _WAVERR_SYNC: + return "WAVERR_SYNC" + } + return fmt.Sprintf("MMRESULT (%d)", m) +} + +func waveOutOpen(f *_WAVEFORMATEX, callback uintptr) (uintptr, error) { + const ( + waveMapper = 0xffffffff + callbackFunction = 0x30000 + ) + var w uintptr + var fdwOpen uintptr + if callback != 0 { + fdwOpen |= callbackFunction + } + r, _, e := procWaveOutOpen.Call(uintptr(unsafe.Pointer(&w)), waveMapper, uintptr(unsafe.Pointer(f)), + callback, 0, fdwOpen) + runtime.KeepAlive(f) + if _MMRESULT(r) != _MMSYSERR_NOERROR { + if e != nil && e != windows.ERROR_SUCCESS { + return 0, fmt.Errorf("oto: waveOutOpen failed: %w", e) + } + return 0, fmt.Errorf("oto: waveOutOpen failed: %w", _MMRESULT(r)) + } + return w, nil +} + +func waveOutClose(hwo uintptr) error { + r, _, e := procWaveOutClose.Call(hwo) + if _MMRESULT(r) != _MMSYSERR_NOERROR { + if e != nil && e != windows.ERROR_SUCCESS { + return fmt.Errorf("oto: waveOutClose failed: %w", e) + } + return fmt.Errorf("oto: waveOutClose failed: %w", _MMRESULT(r)) + } + return nil +} + +func waveOutPrepareHeader(hwo uintptr, pwh *_WAVEHDR) error { + r, _, e := procWaveOutPrepareHeader.Call(hwo, uintptr(unsafe.Pointer(pwh)), unsafe.Sizeof(_WAVEHDR{})) + runtime.KeepAlive(pwh) + if _MMRESULT(r) != _MMSYSERR_NOERROR { + if e != nil && e != windows.ERROR_SUCCESS { + return fmt.Errorf("oto: waveOutPrepareHeader failed: %w", e) + } + return fmt.Errorf("oto: waveOutPrepareHeader failed: %w", _MMRESULT(r)) + } + return nil +} + +func waveOutUnprepareHeader(hwo uintptr, pwh *_WAVEHDR) error { + r, _, e := procWaveOutUnprepareHeader.Call(hwo, uintptr(unsafe.Pointer(pwh)), unsafe.Sizeof(_WAVEHDR{})) + runtime.KeepAlive(pwh) + if _MMRESULT(r) != _MMSYSERR_NOERROR { + if e != nil && e != windows.ERROR_SUCCESS { + return fmt.Errorf("oto: waveOutUnprepareHeader failed: %w", e) + } + return fmt.Errorf("oto: waveOutUnprepareHeader failed: %w", _MMRESULT(r)) + } + return nil +} + +func waveOutWrite(hwo uintptr, pwh *_WAVEHDR) error { + r, _, e := procWaveOutWrite.Call(hwo, uintptr(unsafe.Pointer(pwh)), unsafe.Sizeof(_WAVEHDR{})) + runtime.KeepAlive(pwh) + if _MMRESULT(r) != _MMSYSERR_NOERROR { + if e != nil && e != windows.ERROR_SUCCESS { + return fmt.Errorf("oto: waveOutWrite failed: %w", e) + } + return fmt.Errorf("oto: waveOutWrite failed: %w", _MMRESULT(r)) + } + return nil +} diff --git a/vendor/github.com/ebitengine/oto/v3/context.go b/vendor/github.com/ebitengine/oto/v3/context.go new file mode 100644 index 0000000..b91ae5c --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/context.go @@ -0,0 +1,181 @@ +// Copyright 2021 The Oto Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package oto + +import ( + "fmt" + "io" + "sync" + "time" + + "github.com/ebitengine/oto/v3/internal/mux" +) + +var ( + contextCreated bool + contextCreationMutex sync.Mutex +) + +// Context is the main object in Oto. It interacts with the audio drivers. +// +// To play sound with Oto, first create a context. Then use the context to create +// an arbitrary number of players. Then use the players to play sound. +// +// Creating multiple contexts is NOT supported. +type Context struct { + context *context +} + +// Format is the format of sources. +type Format int + +const ( + // FormatFloat32LE is the format of 32 bits floats little endian. + FormatFloat32LE Format = iota + + // FormatUnsignedInt8 is the format of 8 bits integers. + FormatUnsignedInt8 + + //FormatSignedInt16LE is the format of 16 bits integers little endian. + FormatSignedInt16LE +) + +// NewContextOptions represents options for NewContext. +type NewContextOptions struct { + // SampleRate specifies the number of samples that should be played during one second. + // Usual numbers are 44100 or 48000. One context has only one sample rate. You cannot play multiple audio + // sources with different sample rates at the same time. + SampleRate int + + // ChannelCount specifies the number of channels. One channel is mono playback. Two + // channels are stereo playback. No other values are supported. + ChannelCount int + + // Format specifies the format of sources. + Format Format + + // BufferSize specifies a buffer size in the underlying device. + // + // If 0 is specified, the driver's default buffer size is used. + // Set BufferSize to adjust the buffer size if you want to adjust latency or reduce noises. + // Too big buffer size can increase the latency time. + // On the other hand, too small buffer size can cause glitch noises due to buffer shortage. + BufferSize time.Duration +} + +// NewContext creates a new context with given options. +// A context creates and holds ready-to-use Player objects. +// NewContext returns a context, a channel that is closed when the context is ready, and an error if it exists. +// +// Creating multiple contexts is NOT supported. +func NewContext(options *NewContextOptions) (*Context, chan struct{}, error) { + contextCreationMutex.Lock() + defer contextCreationMutex.Unlock() + + if contextCreated { + return nil, nil, fmt.Errorf("oto: context is already created") + } + contextCreated = true + + var bufferSizeInBytes int + if options.BufferSize != 0 { + // The underying driver always uses 32bit floats. + bytesPerSample := options.ChannelCount * 4 + bytesPerSecond := options.SampleRate * bytesPerSample + bufferSizeInBytes = int(int64(options.BufferSize) * int64(bytesPerSecond) / int64(time.Second)) + bufferSizeInBytes = bufferSizeInBytes / bytesPerSample * bytesPerSample + } + ctx, ready, err := newContext(options.SampleRate, options.ChannelCount, mux.Format(options.Format), bufferSizeInBytes) + if err != nil { + return nil, nil, err + } + return &Context{context: ctx}, ready, nil +} + +// NewPlayer creates a new, ready-to-use Player belonging to the Context. +// It is safe to create multiple players. +// +// The format of r is as follows: +// +// [data] = [sample 1] [sample 2] [sample 3] ... +// [sample *] = [channel 1] [channel 2] ... +// [channel *] = [byte 1] [byte 2] ... +// +// Byte ordering is little endian. +// +// A player has some amount of an underlying buffer. +// Read data from r is queued to the player's underlying buffer. +// The underlying buffer is consumed by its playing. +// Then, r's position and the current playing position don't necessarily match. +// If you want to clear the underlying buffer for some reasons e.g., you want to seek the position of r, +// call the player's Reset function. +// +// You cannot share r by multiple players. +// +// The returned player implements Player, BufferSizeSetter, and io.Seeker. +// You can modify the buffer size of a player by the SetBufferSize function. +// A small buffer size is useful if you want to play a real-time PCM for example. +// Note that the audio quality might be affected if you modify the buffer size. +// +// If r does not implement io.Seeker, the returned player's Seek returns an error. +// +// NewPlayer is concurrent-safe. +// +// All the functions of a Player returned by NewPlayer are concurrent-safe. +func (c *Context) NewPlayer(r io.Reader) *Player { + return &Player{ + player: c.context.mux.NewPlayer(r), + } +} + +// Suspend suspends the entire audio play. +// +// Suspend is concurrent-safe. +func (c *Context) Suspend() error { + return c.context.Suspend() +} + +// Resume resumes the entire audio play, which was suspended by Suspend. +// +// Resume is concurrent-safe. +func (c *Context) Resume() error { + return c.context.Resume() +} + +// Err returns the current error. +// +// Err is concurrent-safe. +func (c *Context) Err() error { + return c.context.Err() +} + +type atomicError struct { + err error + m sync.Mutex +} + +func (a *atomicError) TryStore(err error) { + a.m.Lock() + defer a.m.Unlock() + if a.err == nil { + a.err = err + } +} + +func (a *atomicError) Load() error { + a.m.Lock() + defer a.m.Unlock() + return a.err +} diff --git a/vendor/github.com/ebitengine/oto/v3/driver_android.go b/vendor/github.com/ebitengine/oto/v3/driver_android.go new file mode 100644 index 0000000..52e33f5 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/driver_android.go @@ -0,0 +1,49 @@ +// Copyright 2021 The Oto Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package oto + +import ( + "github.com/ebitengine/oto/v3/internal/mux" + "github.com/ebitengine/oto/v3/internal/oboe" +) + +type context struct { + mux *mux.Mux +} + +func newContext(sampleRate int, channelCount int, format mux.Format, bufferSizeInBytes int) (*context, chan struct{}, error) { + ready := make(chan struct{}) + close(ready) + + c := &context{ + mux: mux.New(sampleRate, channelCount, format), + } + if err := oboe.Play(sampleRate, channelCount, c.mux.ReadFloat32s, bufferSizeInBytes); err != nil { + return nil, nil, err + } + return c, ready, nil +} + +func (c *context) Suspend() error { + return oboe.Suspend() +} + +func (c *context) Resume() error { + return oboe.Resume() +} + +func (c *context) Err() error { + return nil +} diff --git a/vendor/github.com/ebitengine/oto/v3/driver_console.go b/vendor/github.com/ebitengine/oto/v3/driver_console.go new file mode 100644 index 0000000..3372766 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/driver_console.go @@ -0,0 +1,74 @@ +// Copyright 2022 The Oto Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build nintendosdk || playstation5 + +package oto + +// #cgo !darwin LDFLAGS: -Wl,-unresolved-symbols=ignore-all +// #cgo darwin LDFLAGS: -Wl,-undefined,dynamic_lookup +// +// typedef void (*oto_OnReadCallbackType)(float* buf, size_t length); +// +// void oto_OpenAudio(int sample_rate, int channel_num, oto_OnReadCallbackType on_read_callback, int buffer_size_in_bytes); +// +// void oto_OnReadCallback(float* buf, size_t length); +// static void oto_OpenAudioProxy(int sample_rate, int channel_num, int buffer_size_in_bytes) { +// oto_OpenAudio(sample_rate, channel_num, oto_OnReadCallback, buffer_size_in_bytes); +// } +import "C" + +import ( + "unsafe" + + "github.com/ebitengine/oto/v3/internal/mux" +) + +//export oto_OnReadCallback +func oto_OnReadCallback(buf *C.float, length C.size_t) { + theContext.mux.ReadFloat32s(unsafe.Slice((*float32)(unsafe.Pointer(buf)), length)) +} + +type context struct { + mux *mux.Mux +} + +var theContext *context + +func newContext(sampleRate int, channelCount int, format mux.Format, bufferSizeInBytes int) (*context, chan struct{}, error) { + ready := make(chan struct{}) + close(ready) + + c := &context{ + mux: mux.New(sampleRate, channelCount, format), + } + theContext = c + C.oto_OpenAudioProxy(C.int(sampleRate), C.int(channelCount), C.int(bufferSizeInBytes)) + + return c, ready, nil +} + +func (c *context) Suspend() error { + // Do nothing so far. + return nil +} + +func (c *context) Resume() error { + // Do nothing so far. + return nil +} + +func (c *context) Err() error { + return nil +} diff --git a/vendor/github.com/ebitengine/oto/v3/driver_darwin.go b/vendor/github.com/ebitengine/oto/v3/driver_darwin.go new file mode 100644 index 0000000..bd1f575 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/driver_darwin.go @@ -0,0 +1,299 @@ +// Copyright 2021 The Oto Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package oto + +import ( + "fmt" + "runtime" + "sync" + "time" + "unsafe" + + "github.com/ebitengine/oto/v3/internal/mux" +) + +const ( + float32SizeInBytes = 4 + + bufferCount = 4 + + noErr = 0 +) + +func newAudioQueue(sampleRate, channelCount int, oneBufferSizeInBytes int) (_AudioQueueRef, []_AudioQueueBufferRef, error) { + desc := _AudioStreamBasicDescription{ + mSampleRate: float64(sampleRate), + mFormatID: uint32(kAudioFormatLinearPCM), + mFormatFlags: uint32(kAudioFormatFlagIsFloat), + mBytesPerPacket: uint32(channelCount * float32SizeInBytes), + mFramesPerPacket: 1, + mBytesPerFrame: uint32(channelCount * float32SizeInBytes), + mChannelsPerFrame: uint32(channelCount), + mBitsPerChannel: uint32(8 * float32SizeInBytes), + } + + var audioQueue _AudioQueueRef + if osstatus := _AudioQueueNewOutput( + &desc, + render, + nil, + 0, //CFRunLoopRef + 0, //CFStringRef + 0, + &audioQueue); osstatus != noErr { + return 0, nil, fmt.Errorf("oto: AudioQueueNewFormat with StreamFormat failed: %d", osstatus) + } + + bufs := make([]_AudioQueueBufferRef, 0, bufferCount) + for len(bufs) < cap(bufs) { + var buf _AudioQueueBufferRef + if osstatus := _AudioQueueAllocateBuffer(audioQueue, uint32(oneBufferSizeInBytes), &buf); osstatus != noErr { + return 0, nil, fmt.Errorf("oto: AudioQueueAllocateBuffer failed: %d", osstatus) + } + buf.mAudioDataByteSize = uint32(oneBufferSizeInBytes) + bufs = append(bufs, buf) + } + + return audioQueue, bufs, nil +} + +type context struct { + audioQueue _AudioQueueRef + unqueuedBuffers []_AudioQueueBufferRef + + oneBufferSizeInBytes int + + cond *sync.Cond + + toPause bool + toResume bool + + mux *mux.Mux + err atomicError +} + +// TODO: Convert the error code correctly. +// See https://stackoverflow.com/questions/2196869/how-do-you-convert-an-iphone-osstatus-code-to-something-useful + +var theContext *context + +func newContext(sampleRate int, channelCount int, format mux.Format, bufferSizeInBytes int) (*context, chan struct{}, error) { + // defaultOneBufferSizeInBytes is the default buffer size in bytes. + // + // 12288 seems necessary at least on iPod touch (7th) and MacBook Pro 2020. + // With 48000[Hz] stereo, the maximum delay is (12288*4[buffers] / 4 / 2)[samples] / 48000 [Hz] = 100[ms]. + // '4' is float32 size in bytes. '2' is a number of channels for stereo. + const defaultOneBufferSizeInBytes = 12288 + + var oneBufferSizeInBytes int + if bufferSizeInBytes != 0 { + oneBufferSizeInBytes = bufferSizeInBytes / bufferCount + } else { + oneBufferSizeInBytes = defaultOneBufferSizeInBytes + } + bytesPerSample := channelCount * 4 + oneBufferSizeInBytes = oneBufferSizeInBytes / bytesPerSample * bytesPerSample + + ready := make(chan struct{}) + + c := &context{ + cond: sync.NewCond(&sync.Mutex{}), + mux: mux.New(sampleRate, channelCount, format), + oneBufferSizeInBytes: oneBufferSizeInBytes, + } + theContext = c + + if err := initializeAPI(); err != nil { + return nil, nil, err + } + + go func() { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + var readyClosed bool + defer func() { + if !readyClosed { + close(ready) + } + }() + + q, bs, err := newAudioQueue(sampleRate, channelCount, oneBufferSizeInBytes) + if err != nil { + c.err.TryStore(err) + return + } + c.audioQueue = q + c.unqueuedBuffers = bs + + var retryCount int + try: + if osstatus := _AudioQueueStart(c.audioQueue, nil); osstatus != noErr { + if osstatus == avAudioSessionErrorCodeCannotStartPlaying && retryCount < 100 { + // TODO: use sleepTime() after investigating when this error happens. + time.Sleep(10 * time.Millisecond) + retryCount++ + goto try + } + c.err.TryStore(fmt.Errorf("oto: AudioQueueStart failed at newContext: %d", osstatus)) + return + } + + close(ready) + readyClosed = true + + c.loop() + }() + + return c, ready, nil +} + +func (c *context) wait() bool { + c.cond.L.Lock() + defer c.cond.L.Unlock() + + for len(c.unqueuedBuffers) == 0 && c.err.Load() == nil && !c.toPause && !c.toResume { + c.cond.Wait() + } + return c.err.Load() == nil +} + +func (c *context) loop() { + buf32 := make([]float32, c.oneBufferSizeInBytes/4) + for { + if !c.wait() { + return + } + c.appendBuffer(buf32) + } +} + +func (c *context) appendBuffer(buf32 []float32) { + c.cond.L.Lock() + defer c.cond.L.Unlock() + + if c.err.Load() != nil { + return + } + + if c.toPause { + if err := c.pause(); err != nil { + c.err.TryStore(err) + } + c.toPause = false + return + } + + if c.toResume { + if err := c.resume(); err != nil { + c.err.TryStore(err) + } + c.toResume = false + return + } + + buf := c.unqueuedBuffers[0] + copy(c.unqueuedBuffers, c.unqueuedBuffers[1:]) + c.unqueuedBuffers = c.unqueuedBuffers[:len(c.unqueuedBuffers)-1] + + c.mux.ReadFloat32s(buf32) + copy(unsafe.Slice((*float32)(unsafe.Pointer(buf.mAudioData)), buf.mAudioDataByteSize/float32SizeInBytes), buf32) + + if osstatus := _AudioQueueEnqueueBuffer(c.audioQueue, buf, 0, nil); osstatus != noErr { + c.err.TryStore(fmt.Errorf("oto: AudioQueueEnqueueBuffer failed: %d", osstatus)) + } +} + +func (c *context) Suspend() error { + c.cond.L.Lock() + defer c.cond.L.Unlock() + + if err := c.err.Load(); err != nil { + return err.(error) + } + c.toPause = true + c.toResume = false + c.cond.Signal() + return nil +} + +func (c *context) Resume() error { + c.cond.L.Lock() + defer c.cond.L.Unlock() + + if err := c.err.Load(); err != nil { + return err.(error) + } + c.toPause = false + c.toResume = true + c.cond.Signal() + return nil +} + +func (c *context) pause() error { + if osstatus := _AudioQueuePause(c.audioQueue); osstatus != noErr { + return fmt.Errorf("oto: AudioQueuePause failed: %d", osstatus) + } + return nil +} + +func (c *context) resume() error { + var retryCount int +try: + if osstatus := _AudioQueueStart(c.audioQueue, nil); osstatus != noErr { + if (osstatus == avAudioSessionErrorCodeCannotStartPlaying || + osstatus == avAudioSessionErrorCodeCannotInterruptOthers) && + retryCount < 30 { + // It is uncertain that this error is temporary or not. Then let's use exponential-time sleeping. + time.Sleep(sleepTime(retryCount)) + retryCount++ + goto try + } + if osstatus == avAudioSessionErrorCodeSiriIsRecording { + // As this error should be temporary, it should be OK to use a short time for sleep anytime. + time.Sleep(10 * time.Millisecond) + goto try + } + return fmt.Errorf("oto: AudioQueueStart failed at Resume: %d", osstatus) + } + return nil +} + +func (c *context) Err() error { + if err := c.err.Load(); err != nil { + return err.(error) + } + return nil +} + +func render(inUserData unsafe.Pointer, inAQ _AudioQueueRef, inBuffer _AudioQueueBufferRef) { + theContext.cond.L.Lock() + defer theContext.cond.L.Unlock() + theContext.unqueuedBuffers = append(theContext.unqueuedBuffers, inBuffer) + theContext.cond.Signal() +} + +func sleepTime(count int) time.Duration { + switch count { + case 0: + return 10 * time.Millisecond + case 1: + return 20 * time.Millisecond + case 2: + return 50 * time.Millisecond + default: + return 100 * time.Millisecond + } +} diff --git a/vendor/github.com/ebitengine/oto/v3/driver_js.go b/vendor/github.com/ebitengine/oto/v3/driver_js.go new file mode 100644 index 0000000..e1888d7 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/driver_js.go @@ -0,0 +1,223 @@ +// Copyright 2021 The Oto Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package oto + +import ( + "errors" + "fmt" + "runtime" + "syscall/js" + "unsafe" + + "github.com/ebitengine/oto/v3/internal/mux" +) + +type context struct { + audioContext js.Value + scriptProcessor js.Value + scriptProcessorCallback js.Func + ready bool + + mux *mux.Mux +} + +func newContext(sampleRate int, channelCount int, format mux.Format, bufferSizeInBytes int) (*context, chan struct{}, error) { + ready := make(chan struct{}) + + class := js.Global().Get("AudioContext") + if !class.Truthy() { + class = js.Global().Get("webkitAudioContext") + } + if !class.Truthy() { + return nil, nil, errors.New("oto: AudioContext or webkitAudioContext was not found") + } + options := js.Global().Get("Object").New() + options.Set("sampleRate", sampleRate) + + d := &context{ + audioContext: class.New(options), + mux: mux.New(sampleRate, channelCount, format), + } + + if bufferSizeInBytes == 0 { + // 4096 was not great at least on Safari 15. + bufferSizeInBytes = 8192 * channelCount + } + + buf32 := make([]float32, bufferSizeInBytes/4) + + if w := d.audioContext.Get("audioWorklet"); w.Truthy() { + script := fmt.Sprintf(` +class OtoWorkletProcessor extends AudioWorkletProcessor { + constructor() { + super(); + this.bufferSize_ = %[1]d; + this.channelCount_ = %[2]d; + this.buf_ = new Float32Array(); + this.waitRecv_ = false; + + // Receive data from the main thread. + this.port.onmessage = (event) => { + const buf = event.data; + const newBuf = new Float32Array(this.buf_.length + buf.length); + newBuf.set(this.buf_); + newBuf.set(buf, this.buf_.length); + this.buf_ = newBuf; + this.waitRecv_ = false; + } + } + + process(inputs, outputs, parameters) { + const output = outputs[0]; + + // If the buffer is too short, request more data and return silence. + if (this.buf_.length < output[0].length*this.channelCount_) { + if (!this.waitRecv_) { + this.waitRecv_ = true; + this.port.postMessage(null); + } + for (let i = 0; i < output.length; i++) { + output[i].fill(0); + } + return true; + } + + // If the buffer is short, request more data. + if (this.buf_.length < this.bufferSize_*this.channelCount_ / 2 && !this.waitRecv_) { + this.waitRecv_ = true; + this.port.postMessage(null); + } + + for (let i = 0; i < this.channelCount_; i++) { + for (let j = 0; j < output[i].length; j++) { + output[i][j] = this.buf_[j*this.channelCount_+i]; + } + } + this.buf_ = this.buf_.slice(output[0].length*this.channelCount_); + return true; + } +} +registerProcessor('oto-worklet-processor', OtoWorkletProcessor); +`, bufferSizeInBytes/4/channelCount, channelCount) + w.Call("addModule", newScriptURL(script)).Call("then", js.FuncOf(func(this js.Value, arguments []js.Value) any { + node := js.Global().Get("AudioWorkletNode").New(d.audioContext, "oto-worklet-processor", map[string]any{ + "outputChannelCount": []any{channelCount}, + }) + port := node.Get("port") + // When the worklet processor requests more data, send the request to the worklet. + port.Set("onmessage", js.FuncOf(func(this js.Value, arguments []js.Value) any { + d.mux.ReadFloat32s(buf32) + buf := float32SliceToTypedArray(buf32) + port.Call("postMessage", buf, map[string]any{ + "transfer": []any{buf.Get("buffer")}, + }) + return nil + })) + node.Call("connect", d.audioContext.Get("destination")) + return nil + })) + } else { + // Use ScriptProcessorNode if AudioWorklet is not available. + + chBuf32 := make([][]float32, channelCount) + for i := range chBuf32 { + chBuf32[i] = make([]float32, len(buf32)/channelCount) + } + + sp := d.audioContext.Call("createScriptProcessor", bufferSizeInBytes/4/channelCount, 0, channelCount) + f := js.FuncOf(func(this js.Value, arguments []js.Value) any { + d.mux.ReadFloat32s(buf32) + for i := 0; i < channelCount; i++ { + for j := range chBuf32[i] { + chBuf32[i][j] = buf32[j*channelCount+i] + } + } + + buf := arguments[0].Get("outputBuffer") + if buf.Get("copyToChannel").Truthy() { + for i := 0; i < channelCount; i++ { + buf.Call("copyToChannel", float32SliceToTypedArray(chBuf32[i]), i, 0) + } + } else { + // copyToChannel is not defined on Safari 11. + for i := 0; i < channelCount; i++ { + buf.Call("getChannelData", i).Call("set", float32SliceToTypedArray(chBuf32[i])) + } + } + return nil + }) + sp.Call("addEventListener", "audioprocess", f) + d.scriptProcessor = sp + d.scriptProcessorCallback = f + sp.Call("connect", d.audioContext.Get("destination")) + } + + // Browsers require user interaction to start the audio. + // https://developers.google.com/web/updates/2017/09/autoplay-policy-changes#webaudio + + events := []string{"touchend", "keyup", "mouseup"} + + var onEventFired js.Func + var onResumeSuccess js.Func + onResumeSuccess = js.FuncOf(func(this js.Value, arguments []js.Value) any { + d.ready = true + close(ready) + for _, event := range events { + js.Global().Get("document").Call("removeEventListener", event, onEventFired) + } + onEventFired.Release() + onResumeSuccess.Release() + return nil + }) + onEventFired = js.FuncOf(func(this js.Value, arguments []js.Value) any { + if !d.ready { + d.audioContext.Call("resume").Call("then", onResumeSuccess) + } + return nil + }) + for _, event := range events { + js.Global().Get("document").Call("addEventListener", event, onEventFired) + } + + return d, ready, nil +} + +func (c *context) Suspend() error { + c.audioContext.Call("suspend") + return nil +} + +func (c *context) Resume() error { + c.audioContext.Call("resume") + return nil +} + +func (c *context) Err() error { + return nil +} + +func float32SliceToTypedArray(s []float32) js.Value { + bs := unsafe.Slice((*byte)(unsafe.Pointer(&s[0])), len(s)*4) + a := js.Global().Get("Uint8Array").New(len(bs)) + js.CopyBytesToJS(a, bs) + runtime.KeepAlive(s) + buf := a.Get("buffer") + return js.Global().Get("Float32Array").New(buf, a.Get("byteOffset"), a.Get("byteLength").Int()/4) +} + +func newScriptURL(script string) js.Value { + blob := js.Global().Get("Blob").New([]any{script}, map[string]any{"type": "text/javascript"}) + return js.Global().Get("URL").Call("createObjectURL", blob) +} diff --git a/vendor/github.com/ebitengine/oto/v3/driver_nintendosdk.cpp b/vendor/github.com/ebitengine/oto/v3/driver_nintendosdk.cpp new file mode 100644 index 0000000..a087286 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/driver_nintendosdk.cpp @@ -0,0 +1,25 @@ +// Copyright 2022 The Oto Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build nintendosdk + +// The actual implementaiton will be provided by github.com/hajimehoshi/uwagaki. + +#include + +typedef void (*oto_OnReadCallbackType)(float *buf, size_t length); + +extern "C" void oto_OpenAudio(int sample_rate, int channel_num, + oto_OnReadCallbackType on_read_callback, + int buffer_size_in_bytes) {} diff --git a/vendor/github.com/ebitengine/oto/v3/driver_playstation5.cpp b/vendor/github.com/ebitengine/oto/v3/driver_playstation5.cpp new file mode 100644 index 0000000..54ca64d --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/driver_playstation5.cpp @@ -0,0 +1,25 @@ +// Copyright 2025 The Oto Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build playstation5 + +// The actual implementaiton will be provided by github.com/hajimehoshi/uwagaki. + +#include + +typedef void (*oto_OnReadCallbackType)(float *buf, size_t length); + +extern "C" void oto_OpenAudio(int sample_rate, int channel_num, + oto_OnReadCallbackType on_read_callback, + int buffer_size_in_bytes) {} diff --git a/vendor/github.com/ebitengine/oto/v3/driver_unix.go b/vendor/github.com/ebitengine/oto/v3/driver_unix.go new file mode 100644 index 0000000..5b40195 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/driver_unix.go @@ -0,0 +1,278 @@ +// Copyright 2021 The Oto Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !android && !darwin && !js && !windows && !nintendosdk && !playstation5 + +package oto + +// #cgo pkg-config: alsa +// +// #include +import "C" + +import ( + "fmt" + "strings" + "sync" + "unsafe" + + "github.com/ebitengine/oto/v3/internal/mux" +) + +type context struct { + channelCount int + + suspended bool + + handle *C.snd_pcm_t + + cond *sync.Cond + + mux *mux.Mux + err atomicError + + ready chan struct{} +} + +var theContext *context + +func alsaError(name string, err C.int) error { + return fmt.Errorf("oto: ALSA error at %s: %s", name, C.GoString(C.snd_strerror(err))) +} + +func deviceCandidates() []string { + const getAllDevices = -1 + + cPCMInterfaceName := C.CString("pcm") + defer C.free(unsafe.Pointer(cPCMInterfaceName)) + + var hints *unsafe.Pointer + err := C.snd_device_name_hint(getAllDevices, cPCMInterfaceName, &hints) + if err != 0 { + return []string{"default", "plug:default"} + } + defer C.snd_device_name_free_hint(hints) + + var devices []string + + cIoHintName := C.CString("IOID") + defer C.free(unsafe.Pointer(cIoHintName)) + cNameHintName := C.CString("NAME") + defer C.free(unsafe.Pointer(cNameHintName)) + + for it := hints; *it != nil; it = (*unsafe.Pointer)(unsafe.Pointer(uintptr(unsafe.Pointer(it)) + unsafe.Sizeof(uintptr(0)))) { + io := C.snd_device_name_get_hint(*it, cIoHintName) + defer func() { + if io != nil { + C.free(unsafe.Pointer(io)) + } + }() + if C.GoString(io) == "Input" { + continue + } + + name := C.snd_device_name_get_hint(*it, cNameHintName) + defer func() { + if name != nil { + C.free(unsafe.Pointer(name)) + } + }() + if name == nil { + continue + } + goName := C.GoString(name) + if goName == "null" { + continue + } + if goName == "default" { + continue + } + devices = append(devices, goName) + } + + devices = append([]string{"default", "plug:default"}, devices...) + + return devices +} + +func newContext(sampleRate int, channelCount int, format mux.Format, bufferSizeInBytes int) (*context, chan struct{}, error) { + c := &context{ + channelCount: channelCount, + cond: sync.NewCond(&sync.Mutex{}), + mux: mux.New(sampleRate, channelCount, format), + ready: make(chan struct{}), + } + theContext = c + + go func() { + defer close(c.ready) + + // Open a default ALSA audio device for blocking stream playback + type openError struct { + device string + err C.int + } + var openErrs []openError + var found bool + + for _, name := range deviceCandidates() { + cname := C.CString(name) + defer C.free(unsafe.Pointer(cname)) + if err := C.snd_pcm_open(&c.handle, cname, C.SND_PCM_STREAM_PLAYBACK, 0); err < 0 { + openErrs = append(openErrs, openError{ + device: name, + err: err, + }) + continue + } + found = true + break + } + if !found { + var msgs []string + for _, e := range openErrs { + msgs = append(msgs, fmt.Sprintf("%q: %s", e.device, C.GoString(C.snd_strerror(e.err)))) + } + c.err.TryStore(fmt.Errorf("oto: ALSA error at snd_pcm_open: %s", strings.Join(msgs, ", "))) + return + } + + // TODO: Should snd_pcm_hw_params_set_periods be called explicitly? + const periods = 2 + var periodSize C.snd_pcm_uframes_t + if bufferSizeInBytes != 0 { + periodSize = C.snd_pcm_uframes_t(bufferSizeInBytes / (channelCount * 4 * periods)) + } else { + periodSize = C.snd_pcm_uframes_t(1024) + } + bufferSize := periodSize * periods + if err := c.alsaPcmHwParams(sampleRate, channelCount, &bufferSize, &periodSize); err != nil { + c.err.TryStore(err) + return + } + + go func() { + buf32 := make([]float32, int(periodSize)*channelCount) + for { + if !c.readAndWrite(buf32) { + return + } + } + }() + }() + + return c, c.ready, nil +} + +func (c *context) alsaPcmHwParams(sampleRate, channelCount int, bufferSize, periodSize *C.snd_pcm_uframes_t) error { + var params *C.snd_pcm_hw_params_t + C.snd_pcm_hw_params_malloc(¶ms) + defer C.free(unsafe.Pointer(params)) + + if err := C.snd_pcm_hw_params_any(c.handle, params); err < 0 { + return alsaError("snd_pcm_hw_params_any", err) + } + if err := C.snd_pcm_hw_params_set_access(c.handle, params, C.SND_PCM_ACCESS_RW_INTERLEAVED); err < 0 { + return alsaError("snd_pcm_hw_params_set_access", err) + } + if err := C.snd_pcm_hw_params_set_format(c.handle, params, C.SND_PCM_FORMAT_FLOAT_LE); err < 0 { + return alsaError("snd_pcm_hw_params_set_format", err) + } + if err := C.snd_pcm_hw_params_set_channels(c.handle, params, C.unsigned(channelCount)); err < 0 { + return alsaError("snd_pcm_hw_params_set_channels", err) + } + if err := C.snd_pcm_hw_params_set_rate_resample(c.handle, params, 1); err < 0 { + return alsaError("snd_pcm_hw_params_set_rate_resample", err) + } + sr := C.unsigned(sampleRate) + if err := C.snd_pcm_hw_params_set_rate_near(c.handle, params, &sr, nil); err < 0 { + return alsaError("snd_pcm_hw_params_set_rate_near", err) + } + if err := C.snd_pcm_hw_params_set_buffer_size_near(c.handle, params, bufferSize); err < 0 { + return alsaError("snd_pcm_hw_params_set_buffer_size_near", err) + } + if err := C.snd_pcm_hw_params_set_period_size_near(c.handle, params, periodSize, nil); err < 0 { + return alsaError("snd_pcm_hw_params_set_period_size_near", err) + } + if err := C.snd_pcm_hw_params(c.handle, params); err < 0 { + return alsaError("snd_pcm_hw_params", err) + } + return nil +} + +func (c *context) readAndWrite(buf32 []float32) bool { + c.cond.L.Lock() + defer c.cond.L.Unlock() + + for c.suspended && c.err.Load() == nil { + c.cond.Wait() + } + if c.err.Load() != nil { + return false + } + + c.mux.ReadFloat32s(buf32) + + for len(buf32) > 0 { + n := C.snd_pcm_writei(c.handle, unsafe.Pointer(&buf32[0]), C.snd_pcm_uframes_t(len(buf32)/c.channelCount)) + if n < 0 { + n = C.long(C.snd_pcm_recover(c.handle, C.int(n), 1)) + } + if n < 0 { + c.err.TryStore(alsaError("snd_pcm_writei or snd_pcm_recover", C.int(n))) + return false + } + buf32 = buf32[int(n)*c.channelCount:] + } + return true +} + +func (c *context) Suspend() error { + <-c.ready + + c.cond.L.Lock() + defer c.cond.L.Unlock() + + if err := c.err.Load(); err != nil { + return err.(error) + } + + c.suspended = true + + // Do not use snd_pcm_pause as not all devices support this. + // Do not use snd_pcm_drop as this might hang (https://github.com/libsdl-org/SDL/blob/a5c610b0a3857d3138f3f3da1f6dc3172c5ea4a8/src/audio/alsa/SDL_alsa_audio.c#L478). + return nil +} + +func (c *context) Resume() error { + <-c.ready + + c.cond.L.Lock() + defer c.cond.L.Unlock() + + if err := c.err.Load(); err != nil { + return err.(error) + } + + c.suspended = false + c.cond.Signal() + return nil +} + +func (c *context) Err() error { + if err := c.err.Load(); err != nil { + return err.(error) + } + return nil +} diff --git a/vendor/github.com/ebitengine/oto/v3/driver_wasapi_windows.go b/vendor/github.com/ebitengine/oto/v3/driver_wasapi_windows.go new file mode 100644 index 0000000..bfe4a3e --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/driver_wasapi_windows.go @@ -0,0 +1,470 @@ +// Copyright 2022 The Oto Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package oto + +import ( + "errors" + "fmt" + "runtime" + "sync" + "syscall" + "time" + "unsafe" + + "golang.org/x/sys/windows" + + "github.com/ebitengine/oto/v3/internal/mux" +) + +type comThread struct { + funcCh chan func() +} + +func newCOMThread() (*comThread, error) { + funcCh := make(chan func()) + errCh := make(chan error) + go func() { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + // S_FALSE is returned when CoInitializeEx is nested. This is a successful case. + if err := windows.CoInitializeEx(0, windows.COINIT_MULTITHREADED); err != nil && !errors.Is(err, syscall.Errno(windows.S_FALSE)) { + errCh <- err + } + // CoUninitialize should be called even when CoInitializeEx returns S_FALSE. + defer windows.CoUninitialize() + + close(errCh) + + for f := range funcCh { + f() + } + }() + + if err := <-errCh; err != nil { + return nil, err + } + + return &comThread{ + funcCh: funcCh, + }, nil +} + +func (c *comThread) Run(f func()) { + ch := make(chan struct{}) + c.funcCh <- func() { + f() + close(ch) + } + <-ch +} + +type wasapiContext struct { + sampleRate int + channelCount int + mux *mux.Mux + bufferSizeInBytes int + + comThread *comThread + err atomicError + suspended bool + suspendedCond *sync.Cond + + sampleReadyEvent windows.Handle + client *_IAudioClient2 + bufferFrames uint32 + renderClient *_IAudioRenderClient + currentDeviceID string + enumerator *_IMMDeviceEnumerator + + buf []float32 + + m sync.Mutex +} + +var ( + errDeviceSwitched = errors.New("oto: device switched") + errFormatNotSupported = errors.New("oto: the specified format is not supported (there is the closest format instead)") +) + +func newWASAPIContext(sampleRate, channelCount int, mux *mux.Mux, bufferSizeInBytes int) (context *wasapiContext, ferr error) { + t, err := newCOMThread() + if err != nil { + return nil, err + } + + c := &wasapiContext{ + sampleRate: sampleRate, + channelCount: channelCount, + mux: mux, + bufferSizeInBytes: bufferSizeInBytes, + comThread: t, + suspendedCond: sync.NewCond(&sync.Mutex{}), + } + + ev, err := windows.CreateEventEx(nil, nil, 0, windows.EVENT_ALL_ACCESS) + if err != nil { + return nil, err + } + defer func() { + if ferr != nil { + windows.CloseHandle(ev) + } + }() + c.sampleReadyEvent = ev + + if err := c.start(); err != nil { + return nil, err + } + + return c, nil +} + +func (c *wasapiContext) isDeviceSwitched() (bool, error) { + // If the audio is suspended, do nothing. + if c.isSuspended() { + return false, nil + } + + var switched bool + var cerr error + c.comThread.Run(func() { + device, err := c.enumerator.GetDefaultAudioEndPoint(eRender, eConsole) + if err != nil { + cerr = err + return + } + defer device.Release() + + id, err := device.GetId() + if err != nil { + cerr = err + return + } + + if c.currentDeviceID == id { + return + } + switched = true + }) + + return switched, cerr +} + +func (c *wasapiContext) start() error { + var cerr error + c.comThread.Run(func() { + if err := c.startOnCOMThread(); err != nil { + cerr = err + return + } + }) + if cerr != nil { + return cerr + } + + go func() { + if err := c.loop(); err != nil { + if !errors.Is(err, _AUDCLNT_E_DEVICE_INVALIDATED) && !errors.Is(err, _AUDCLNT_E_RESOURCES_INVALIDATED) && !errors.Is(err, errDeviceSwitched) { + c.err.TryStore(err) + return + } + + if err := c.restart(); err != nil { + c.err.TryStore(err) + return + } + } + }() + + return nil +} + +func (c *wasapiContext) startOnCOMThread() (ferr error) { + if c.enumerator == nil { + e, err := _CoCreateInstance(&uuidMMDeviceEnumerator, nil, uint32(_CLSCTX_ALL), &uuidIMMDeviceEnumerator) + if err != nil { + return err + } + c.enumerator = (*_IMMDeviceEnumerator)(e) + defer func() { + if ferr != nil { + c.enumerator.Release() + c.enumerator = nil + } + }() + } + + device, err := c.enumerator.GetDefaultAudioEndPoint(eRender, eConsole) + if err != nil { + if errors.Is(err, _E_NOTFOUND) { + return errDeviceNotFound + } + return err + } + defer device.Release() + + id, err := device.GetId() + if err != nil { + return err + } + c.currentDeviceID = id + + if c.client != nil { + c.client.Release() + c.client = nil + } + + client, err := device.Activate(&uuidIAudioClient2, uint32(_CLSCTX_ALL), nil) + if err != nil { + return err + } + c.client = (*_IAudioClient2)(client) + + if err := c.client.SetClientProperties(&_AudioClientProperties{ + cbSize: uint32(unsafe.Sizeof(_AudioClientProperties{})), + bIsOffload: 0, // false + eCategory: _AudioCategory_Other, // In the example, AudioCategory_ForegroundOnlyMedia was used, but this value is deprecated. + }); err != nil { + return err + } + + // Check the format is supported by WASAPI. + // Stereo with 48000 [Hz] is likely supported, but mono and/or other sample rates are unlikely supported. + // Fallback to WinMM in this case anyway. + const bitsPerSample = 32 + nBlockAlign := c.channelCount * bitsPerSample / 8 + var channelMask uint32 + switch c.channelCount { + case 1: + channelMask = _SPEAKER_FRONT_CENTER + case 2: + channelMask = _SPEAKER_FRONT_LEFT | _SPEAKER_FRONT_RIGHT + } + f := &_WAVEFORMATEXTENSIBLE{ + wFormatTag: _WAVE_FORMAT_EXTENSIBLE, + nChannels: uint16(c.channelCount), + nSamplesPerSec: uint32(c.sampleRate), + nAvgBytesPerSec: uint32(c.sampleRate * nBlockAlign), + nBlockAlign: uint16(nBlockAlign), + wBitsPerSample: bitsPerSample, + cbSize: 0x16, + Samples: bitsPerSample, + dwChannelMask: channelMask, + SubFormat: _KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, + } + + var bufferSizeIn100ns _REFERENCE_TIME + if c.bufferSizeInBytes != 0 { + bufferSizeInFrames := int64(c.bufferSizeInBytes) / int64(nBlockAlign) + bufferSizeIn100ns = _REFERENCE_TIME(1e7 * bufferSizeInFrames / int64(c.sampleRate)) + } else { + // The default buffer size can be too small and might cause glitch noises. + // Specify 50[ms] as the buffer size. + bufferSizeIn100ns = _REFERENCE_TIME(50 * time.Millisecond / 100) + } + + // Even if the sample rate and/or the number of channels are not supported by the audio driver, + // AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM should convert the sample rate automatically (#215). + if err := c.client.Initialize(_AUDCLNT_SHAREMODE_SHARED, + _AUDCLNT_STREAMFLAGS_EVENTCALLBACK|_AUDCLNT_STREAMFLAGS_NOPERSIST|_AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM, + bufferSizeIn100ns, 0, f, nil); err != nil { + return err + } + + frames, err := c.client.GetBufferSize() + if err != nil { + return err + } + c.bufferFrames = frames + + if c.renderClient != nil { + c.renderClient.Release() + c.renderClient = nil + } + + renderClient, err := c.client.GetService(&uuidIAudioRenderClient) + if err != nil { + return err + } + c.renderClient = (*_IAudioRenderClient)(renderClient) + + if err := c.client.SetEventHandle(c.sampleReadyEvent); err != nil { + return err + } + + // TODO: Should some errors be allowed? See WASAPIManager.cpp in the official example SimpleWASAPIPlaySound. + + if err := c.client.Start(); err != nil { + return err + } + + return nil +} + +func (c *wasapiContext) loop() error { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + // S_FALSE is returned when CoInitializeEx is nested. This is a successful case. + if err := windows.CoInitializeEx(0, windows.COINIT_MULTITHREADED); err != nil && !errors.Is(err, syscall.Errno(windows.S_FALSE)) { + _, _ = c.client.Stop() + return err + } + // CoUninitialize should be called even when CoInitializeEx returns S_FALSE. + defer windows.CoUninitialize() + + if err := c.loopOnRenderThread(); err != nil { + _, _ = c.client.Stop() + return err + } + + return nil +} + +func (c *wasapiContext) loopOnRenderThread() error { + last := time.Now() + for { + c.suspendedCond.L.Lock() + for c.suspended { + c.suspendedCond.Wait() + } + c.suspendedCond.L.Unlock() + + evt, err := windows.WaitForSingleObject(c.sampleReadyEvent, windows.INFINITE) + if err != nil { + return err + } + if evt != windows.WAIT_OBJECT_0 { + return fmt.Errorf("oto: WaitForSingleObject failed: returned value: %d", evt) + } + + if err := c.writeOnRenderThread(); err != nil { + return err + } + + // Checking the current default audio device might be an expensive operation. + // Check this repeatedly but with some time interval. + if now := time.Now(); now.Sub(last) >= 500*time.Millisecond { + switched, err := c.isDeviceSwitched() + if err != nil { + return err + } + if switched { + return errDeviceSwitched + } + last = now + } + } +} + +func (c *wasapiContext) writeOnRenderThread() error { + c.m.Lock() + defer c.m.Unlock() + + paddingFrames, err := c.client.GetCurrentPadding() + if err != nil { + return err + } + + frames := c.bufferFrames - paddingFrames + if frames <= 0 { + return nil + } + + // Get the destination buffer. + dstBuf, err := c.renderClient.GetBuffer(frames) + if err != nil { + return err + } + + // Calculate the buffer size. + if buflen := int(frames) * c.channelCount; cap(c.buf) < buflen { + c.buf = make([]float32, buflen) + } else { + c.buf = c.buf[:buflen] + } + + // Read the buffer from the players. + c.mux.ReadFloat32s(c.buf) + + // Copy the read buf to the destination buffer. + copy(unsafe.Slice((*float32)(unsafe.Pointer(dstBuf)), len(c.buf)), c.buf) + + // Release the buffer. + if err := c.renderClient.ReleaseBuffer(frames, 0); err != nil { + return err + } + + c.buf = c.buf[:0] + return nil +} + +func (c *wasapiContext) Suspend() error { + c.suspendedCond.L.Lock() + c.suspended = true + c.suspendedCond.L.Unlock() + c.suspendedCond.Signal() + + return nil +} + +func (c *wasapiContext) Resume() error { + c.suspendedCond.L.Lock() + c.suspended = false + c.suspendedCond.L.Unlock() + c.suspendedCond.Signal() + + return nil +} + +func (c *wasapiContext) isSuspended() bool { + c.suspendedCond.L.Lock() + defer c.suspendedCond.L.Unlock() + return c.suspended +} + +func (c *wasapiContext) Err() error { + return c.err.Load() +} + +func (c *wasapiContext) restart() error { + // Probably the driver is missing temporarily e.g. plugging out the headset. + // Recreate the device. + +retry: + c.suspendedCond.L.Lock() + for c.suspended { + c.suspendedCond.Wait() + } + c.suspendedCond.L.Unlock() + + if err := c.start(); err != nil { + // When a device is switched, the new device might not support the desired format, + // or all the audio devices might be disconnected. + // Instead of aborting this context, let's wait for the next device switch. + if !errors.Is(err, errFormatNotSupported) && !errors.Is(err, errDeviceNotFound) { + return err + } + + // Just read the buffer and discard it. Then, retry to search the device. + var buf32 [4096]float32 + sleep := time.Duration(float64(time.Second) * float64(len(buf32)) / float64(c.channelCount) / float64(c.sampleRate)) + c.mux.ReadFloat32s(buf32[:]) + time.Sleep(sleep) + goto retry + } + return nil +} diff --git a/vendor/github.com/ebitengine/oto/v3/driver_windows.go b/vendor/github.com/ebitengine/oto/v3/driver_windows.go new file mode 100644 index 0000000..1a5abe1 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/driver_windows.go @@ -0,0 +1,163 @@ +// Copyright 2022 The Oto Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package oto + +import ( + "errors" + "fmt" + "time" + + "github.com/ebitengine/oto/v3/internal/mux" +) + +var errDeviceNotFound = errors.New("oto: device not found") + +type context struct { + sampleRate int + channelCount int + + mux *mux.Mux + + wasapiContext *wasapiContext + winmmContext *winmmContext + nullContext *nullContext + + ready chan struct{} + err atomicError +} + +func newContext(sampleRate int, channelCount int, format mux.Format, bufferSizeInBytes int) (*context, chan struct{}, error) { + ctx := &context{ + sampleRate: sampleRate, + channelCount: channelCount, + mux: mux.New(sampleRate, channelCount, format), + ready: make(chan struct{}), + } + + // Initializing drivers might take some time. Do this asynchronously. + go func() { + defer close(ctx.ready) + + xc, err0 := newWASAPIContext(sampleRate, channelCount, ctx.mux, bufferSizeInBytes) + if err0 == nil { + ctx.wasapiContext = xc + return + } + + wc, err1 := newWinMMContext(sampleRate, channelCount, ctx.mux, bufferSizeInBytes) + if err1 == nil { + ctx.winmmContext = wc + return + } + + if errors.Is(err0, errDeviceNotFound) && errors.Is(err1, errDeviceNotFound) { + ctx.nullContext = newNullContext(sampleRate, channelCount, ctx.mux) + return + } + + ctx.err.TryStore(fmt.Errorf("oto: initialization failed: WASAPI: %v, WinMM: %v", err0, err1)) + }() + + return ctx, ctx.ready, nil +} + +func (c *context) Suspend() error { + <-c.ready + if c.wasapiContext != nil { + return c.wasapiContext.Suspend() + } + if c.winmmContext != nil { + return c.winmmContext.Suspend() + } + if c.nullContext != nil { + return c.nullContext.Suspend() + } + return nil +} + +func (c *context) Resume() error { + <-c.ready + if c.wasapiContext != nil { + return c.wasapiContext.Resume() + } + if c.winmmContext != nil { + return c.winmmContext.Resume() + } + if c.nullContext != nil { + return c.nullContext.Resume() + } + return nil +} + +func (c *context) Err() error { + if err := c.err.Load(); err != nil { + return err + } + + select { + case <-c.ready: + default: + return nil + } + + if c.wasapiContext != nil { + return c.wasapiContext.Err() + } + if c.winmmContext != nil { + return c.winmmContext.Err() + } + if c.nullContext != nil { + return c.nullContext.Err() + } + return nil +} + +type nullContext struct { + suspended bool +} + +func newNullContext(sampleRate int, channelCount int, mux *mux.Mux) *nullContext { + c := &nullContext{} + go c.loop(sampleRate, channelCount, mux) + return c +} + +func (c *nullContext) loop(sampleRate int, channelCount int, mux *mux.Mux) { + var buf32 [4096]float32 + sleep := time.Duration(float64(time.Second) * float64(len(buf32)) / float64(channelCount) / float64(sampleRate)) + for { + if c.suspended { + time.Sleep(time.Second) + continue + } + + mux.ReadFloat32s(buf32[:]) + time.Sleep(sleep) + } +} + +func (c *nullContext) Suspend() error { + c.suspended = true + return nil +} + +func (c *nullContext) Resume() error { + c.suspended = false + return nil +} + +func (*nullContext) Err() error { + return nil +} diff --git a/vendor/github.com/ebitengine/oto/v3/driver_winmm_windows.go b/vendor/github.com/ebitengine/oto/v3/driver_winmm_windows.go new file mode 100644 index 0000000..04f935e --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/driver_winmm_windows.go @@ -0,0 +1,297 @@ +// Copyright 2021 The Oto Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package oto + +import ( + "errors" + "fmt" + "sync" + "time" + "unsafe" + + "golang.org/x/sys/windows" + + "github.com/ebitengine/oto/v3/internal/mux" +) + +// Avoid goroutines on Windows (hajimehoshi/ebiten#1768). +// Apparently, switching contexts might take longer than other platforms. + +const defaultHeaderBufferSize = 4096 + +type header struct { + waveOut uintptr + buffer []float32 + waveHdr *_WAVEHDR +} + +func newHeader(waveOut uintptr, bufferSizeInBytes int) (*header, error) { + h := &header{ + waveOut: waveOut, + buffer: make([]float32, bufferSizeInBytes/4), + } + h.waveHdr = &_WAVEHDR{ + lpData: uintptr(unsafe.Pointer(&h.buffer[0])), + dwBufferLength: uint32(bufferSizeInBytes), + } + if err := waveOutPrepareHeader(waveOut, h.waveHdr); err != nil { + return nil, err + } + return h, nil +} + +func (h *header) Write(data []float32) error { + copy(h.buffer, data) + if err := waveOutWrite(h.waveOut, h.waveHdr); err != nil { + return err + } + return nil +} + +func (h *header) IsQueued() bool { + return h.waveHdr.dwFlags&_WHDR_INQUEUE != 0 +} + +func (h *header) Close() error { + return waveOutUnprepareHeader(h.waveOut, h.waveHdr) +} + +type winmmContext struct { + sampleRate int + channelCount int + bufferSizeInBytes int + + waveOut uintptr + headers []*header + + buf32 []float32 + + mux *mux.Mux + err atomicError + loopEndCh chan error + + cond *sync.Cond + + suspended bool + suspendedCond *sync.Cond +} + +var theWinMMContext *winmmContext + +func newWinMMContext(sampleRate, channelCount int, mux *mux.Mux, bufferSizeInBytes int) (*winmmContext, error) { + // winmm.dll is not available on Xbox. + if err := winmm.Load(); err != nil { + return nil, fmt.Errorf("oto: loading winmm.dll failed: %w", err) + } + + c := &winmmContext{ + sampleRate: sampleRate, + channelCount: channelCount, + bufferSizeInBytes: bufferSizeInBytes, + mux: mux, + cond: sync.NewCond(&sync.Mutex{}), + suspendedCond: sync.NewCond(&sync.Mutex{}), + } + theWinMMContext = c + + if err := c.start(); err != nil { + return nil, err + } + return c, nil +} + +func (c *winmmContext) start() error { + const bitsPerSample = 32 + nBlockAlign := c.channelCount * bitsPerSample / 8 + f := &_WAVEFORMATEX{ + wFormatTag: _WAVE_FORMAT_IEEE_FLOAT, + nChannels: uint16(c.channelCount), + nSamplesPerSec: uint32(c.sampleRate), + nAvgBytesPerSec: uint32(c.sampleRate * nBlockAlign), + nBlockAlign: uint16(nBlockAlign), + wBitsPerSample: bitsPerSample, + } + + // TOOD: What about using an event instead of a callback? PortAudio and other libraries do that. + w, err := waveOutOpen(f, waveOutOpenCallback) + if errors.Is(err, windows.ERROR_NOT_FOUND) { + // This can happen when no device is found (#77). + return errDeviceNotFound + } + if errors.Is(err, _MMSYSERR_BADDEVICEID) { + // This can happen when no device is found (hajimehoshi/ebiten#2316). + return errDeviceNotFound + } + if err != nil { + return err + } + + headerBufferSize := defaultHeaderBufferSize + if c.bufferSizeInBytes != 0 { + headerBufferSize = c.bufferSizeInBytes + } + + c.waveOut = w + c.headers = make([]*header, 0, 6) + for len(c.headers) < cap(c.headers) { + h, err := newHeader(c.waveOut, headerBufferSize) + if err != nil { + return err + } + c.headers = append(c.headers, h) + } + + c.buf32 = make([]float32, headerBufferSize/4) + go c.loop() + + return nil +} + +func (c *winmmContext) Suspend() error { + c.suspendedCond.L.Lock() + c.suspended = true + c.suspendedCond.L.Unlock() + c.suspendedCond.Signal() + + return nil +} + +func (c *winmmContext) Resume() (ferr error) { + c.suspendedCond.L.Lock() + c.suspended = false + c.suspendedCond.L.Unlock() + c.suspendedCond.Signal() + + return nil +} + +func (c *winmmContext) Err() error { + if err := c.err.Load(); err != nil { + return err.(error) + } + return nil +} + +func (c *winmmContext) isHeaderAvailable() bool { + for _, h := range c.headers { + if !h.IsQueued() { + return true + } + } + return false +} + +var waveOutOpenCallback = windows.NewCallback(func(hwo, uMsg, dwInstance, dwParam1, dwParam2 uintptr) uintptr { + // Queuing a header in this callback might not work especially when a headset is connected or disconnected. + // Just signal the condition vairable and don't do other things. + const womDone = 0x3bd + if uMsg != womDone { + return 0 + } + theWinMMContext.cond.Signal() + return 0 +}) + +func (c *winmmContext) waitUntilHeaderAvailable() bool { + c.cond.L.Lock() + defer c.cond.L.Unlock() + + for !c.isHeaderAvailable() && c.err.Load() == nil && c.loopEndCh == nil { + c.cond.Wait() + } + return c.err.Load() == nil && c.loopEndCh == nil +} + +func (c *winmmContext) loop() { + defer func() { + if err := c.closeLoop(); err != nil { + c.err.TryStore(err) + } + }() + for { + c.suspendedCond.L.Lock() + for c.suspended { + c.suspendedCond.Wait() + } + c.suspendedCond.L.Unlock() + + if !c.waitUntilHeaderAvailable() { + return + } + c.appendBuffers() + } +} + +func (c *winmmContext) closeLoop() (ferr error) { + c.cond.L.Lock() + defer c.cond.L.Unlock() + + defer func() { + if c.loopEndCh != nil { + if ferr != nil { + c.loopEndCh <- ferr + ferr = nil + } + close(c.loopEndCh) + c.loopEndCh = nil + } + }() + + for _, h := range c.headers { + if err := h.Close(); err != nil { + return err + } + } + c.headers = nil + + if err := waveOutClose(c.waveOut); err != nil { + return err + } + c.waveOut = 0 + return nil +} + +func (c *winmmContext) appendBuffers() { + c.cond.L.Lock() + defer c.cond.L.Unlock() + + if c.err.Load() != nil { + return + } + + c.mux.ReadFloat32s(c.buf32) + + for _, h := range c.headers { + if h.IsQueued() { + continue + } + + if err := h.Write(c.buf32); err != nil { + switch { + case errors.Is(err, _MMSYSERR_NOMEM): + continue + case errors.Is(err, _MMSYSERR_NODRIVER): + sleep := time.Duration(float64(time.Second) * float64(len(c.buf32)) / float64(c.channelCount) / float64(c.sampleRate)) + time.Sleep(sleep) + return + case errors.Is(err, windows.ERROR_NOT_FOUND): + // This error can happen when e.g. a new HDMI connection is detected (#51). + // TODO: Retry later. + } + c.err.TryStore(fmt.Errorf("oto: Queueing the header failed: %v", err)) + } + return + } +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/mux/mux.go b/vendor/github.com/ebitengine/oto/v3/internal/mux/mux.go new file mode 100644 index 0000000..1238104 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/mux/mux.go @@ -0,0 +1,591 @@ +// Copyright 2021 The Oto Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package mux offers APIs for a low-level multiplexer of audio players. +// Usually you don't have to use this package directly. +package mux + +import ( + "errors" + "fmt" + "io" + "math" + "runtime" + "sync" + "time" +) + +// Format must sync with oto's Format. +type Format int + +const ( + FormatFloat32LE Format = iota + FormatUnsignedInt8 + FormatSignedInt16LE +) + +func (f Format) ByteLength() int { + switch f { + case FormatFloat32LE: + return 4 + case FormatUnsignedInt8: + return 1 + case FormatSignedInt16LE: + return 2 + } + panic(fmt.Sprintf("mux: unexpected format: %d", f)) +} + +// Mux is a low-level multiplexer of audio players. +type Mux struct { + sampleRate int + channelCount int + format Format + + players map[*playerImpl]struct{} + cond *sync.Cond +} + +// New creates a new Mux. +func New(sampleRate int, channelCount int, format Format) *Mux { + m := &Mux{ + sampleRate: sampleRate, + channelCount: channelCount, + format: format, + cond: sync.NewCond(&sync.Mutex{}), + } + go m.loop() + return m +} + +func (m *Mux) shouldWait() bool { + for p := range m.players { + if p.canReadSourceToBuffer() { + return false + } + } + return true +} + +func (m *Mux) wait() { + m.cond.L.Lock() + defer m.cond.L.Unlock() + + for m.shouldWait() { + m.cond.Wait() + } +} + +func (m *Mux) loop() { + var players []*playerImpl + for { + m.wait() + + m.cond.L.Lock() + for i := range players { + players[i] = nil + } + players = players[:0] + for p := range m.players { + players = append(players, p) + } + m.cond.L.Unlock() + + allZero := true + for _, p := range players { + n := p.readSourceToBuffer() + if n != 0 { + allZero = false + } + } + + // Sleeping is necessary especially on browsers. + // Sometimes a player continues to read 0 bytes from the source and this loop can be a busy loop in such case. + if allZero { + time.Sleep(time.Millisecond) + } + } +} + +func (m *Mux) addPlayer(player *playerImpl) { + m.cond.L.Lock() + defer m.cond.L.Unlock() + + if m.players == nil { + m.players = map[*playerImpl]struct{}{} + } + m.players[player] = struct{}{} + m.cond.Signal() +} + +func (m *Mux) removePlayer(player *playerImpl) { + m.cond.L.Lock() + defer m.cond.L.Unlock() + + delete(m.players, player) + m.cond.Signal() +} + +// ReadFloat32s fills buf with the multiplexed data of the players as float32 values. +func (m *Mux) ReadFloat32s(buf []float32) { + m.cond.L.Lock() + players := make([]*playerImpl, 0, len(m.players)) + for p := range m.players { + players = append(players, p) + } + m.cond.L.Unlock() + + for i := range buf { + buf[i] = 0 + } + for _, p := range players { + p.readBufferAndAdd(buf) + } + m.cond.Signal() +} + +type Player struct { + p *playerImpl + cleanup runtime.Cleanup +} + +type playerState int + +const ( + playerPaused playerState = iota + playerPlay + playerClosed +) + +type playerImpl struct { + mux *Mux + src io.Reader + prevVolume float64 + volume float64 + err error + state playerState + buf []byte + eof bool + bufferSize int + + m sync.Mutex +} + +func (m *Mux) NewPlayer(src io.Reader) *Player { + pl := &Player{ + p: &playerImpl{ + mux: m, + src: src, + prevVolume: 1, + volume: 1, + bufferSize: m.defaultBufferSize(), + }, + } + pl.cleanup = runtime.AddCleanup(pl, func(p *playerImpl) { + _ = p.Close() + }, pl.p) + return pl +} + +func (p *Player) Err() error { + return p.p.Err() +} + +func (p *playerImpl) Err() error { + p.m.Lock() + defer p.m.Unlock() + + return p.err +} + +func (p *Player) Play() { + p.p.Play() +} + +func (p *playerImpl) Play() { + // Goroutines don't work effiently on Windows. Avoid using them (hajimehoshi/ebiten#1768). + if runtime.GOOS == "windows" { + p.m.Lock() + defer p.m.Unlock() + + p.playImpl() + } else { + ch := make(chan struct{}) + go func() { + p.m.Lock() + defer p.m.Unlock() + + close(ch) + p.playImpl() + }() + <-ch + } +} + +func (p *Player) SetBufferSize(bufferSize int) { + p.p.setBufferSize(bufferSize) +} + +func (p *playerImpl) setBufferSize(bufferSize int) { + p.m.Lock() + defer p.m.Unlock() + + p.bufferSize = bufferSize + if bufferSize == 0 { + p.bufferSize = p.mux.defaultBufferSize() + } +} + +var theBufPool = sync.Pool{ + New: func() interface{} { + var buf []byte + return &buf + }, +} + +func getBufferFromPool(size int) *[]byte { + buf := theBufPool.Get().(*[]byte) + + if cap(*buf) < size { + *buf = make([]byte, size) + } + + *buf = (*buf)[:size] + + return buf +} + +// read reads the source to buf. +// read unlocks the mutex temporarily and locks when reading finishes. +// This avoids locking during an external function call Read (#188). +// +// When read is called, the mutex m must be locked. +func (p *playerImpl) read(buf []byte) (int, error) { + p.m.Unlock() + defer p.m.Lock() + return p.src.Read(buf) +} + +// addToPlayers adds p to the players set. +// +// When addToPlayers is called, the mutex m must be locked. +func (p *playerImpl) addToPlayers() { + p.m.Unlock() + defer p.m.Lock() + p.mux.addPlayer(p) +} + +// removeFromPlayers removes p from the players set. +// +// When removeFromPlayers is called, the mutex m must be locked. +func (p *playerImpl) removeFromPlayers() { + p.m.Unlock() + defer p.m.Lock() + p.mux.removePlayer(p) +} + +func (p *playerImpl) playImpl() { + if p.err != nil { + return + } + if p.state != playerPaused { + return + } + p.state = playerPlay + + if !p.eof { + buf := getBufferFromPool(p.bufferSize) + defer theBufPool.Put(buf) + + if p.buf == nil { + p.buf = (*getBufferFromPool(p.bufferSize))[:0] + } + + for len(p.buf) < p.bufferSize { + n, err := p.read(*buf) + if err != nil && err != io.EOF { + p.setErrorImpl(err) + return + } + p.buf = append(p.buf, (*buf)[:n]...) + if err == io.EOF { + p.eof = true + break + } + } + } + + if p.eof && len(p.buf) == 0 { + p.returnBufferToPool() + p.state = playerPaused + } + + p.addToPlayers() +} + +func (p *Player) Pause() { + p.p.Pause() +} + +func (p *playerImpl) Pause() { + p.m.Lock() + defer p.m.Unlock() + + if p.state != playerPlay { + return + } + p.state = playerPaused +} + +func (p *Player) Seek(offset int64, whence int) (int64, error) { + return p.p.Seek(offset, whence) +} + +func (p *playerImpl) Seek(offset int64, whence int) (int64, error) { + p.m.Lock() + defer p.m.Unlock() + + // If a player is playing, keep playing even after this seeking. + if p.state == playerPlay { + defer p.playImpl() + } + + // Reset the internal buffer. + p.resetImpl() + + // Check if the source implements io.Seeker. + s, ok := p.src.(io.Seeker) + if !ok { + return 0, errors.New("mux: the source must implement io.Seeker") + } + return s.Seek(offset, whence) +} + +func (p *Player) Reset() { + p.p.Reset() +} + +func (p *playerImpl) Reset() { + p.m.Lock() + defer p.m.Unlock() + p.resetImpl() +} + +func (p *playerImpl) resetImpl() { + if p.state == playerClosed { + return + } + p.state = playerPaused + p.buf = p.buf[:0] + p.eof = false +} + +func (p *Player) IsPlaying() bool { + return p.p.IsPlaying() +} + +func (p *playerImpl) IsPlaying() bool { + p.m.Lock() + defer p.m.Unlock() + return p.state == playerPlay +} + +func (p *Player) Volume() float64 { + return p.p.Volume() +} + +func (p *playerImpl) Volume() float64 { + p.m.Lock() + defer p.m.Unlock() + return p.volume +} + +func (p *Player) SetVolume(volume float64) { + p.p.SetVolume(volume) +} + +func (p *playerImpl) SetVolume(volume float64) { + p.m.Lock() + defer p.m.Unlock() + p.volume = volume + if p.state != playerPlay { + p.prevVolume = volume + } +} + +func (p *Player) BufferedSize() int { + return p.p.BufferedSize() +} + +func (p *playerImpl) BufferedSize() int { + p.m.Lock() + defer p.m.Unlock() + return len(p.buf) +} + +func (p *Player) Close() error { + p.cleanup.Stop() + return p.p.Close() +} + +func (p *playerImpl) Close() error { + p.m.Lock() + defer p.m.Unlock() + return p.closeImpl() +} + +func (p *playerImpl) closeImpl() error { + p.removeFromPlayers() + + if p.state == playerClosed { + return p.err + } + p.state = playerClosed + p.returnBufferToPool() + + return p.err +} + +func (p *playerImpl) readBufferAndAdd(buf []float32) int { + p.m.Lock() + defer p.m.Unlock() + + if p.state != playerPlay { + return 0 + } + + format := p.mux.format + bitDepthInBytes := format.ByteLength() + n := len(p.buf) / bitDepthInBytes + if n > len(buf) { + n = len(buf) + } + + prevVolume := float32(p.prevVolume) + volume := float32(p.volume) + + channelCount := p.mux.channelCount + rateDenom := float32(n / channelCount) + + src := p.buf[:n*bitDepthInBytes] + + for i := 0; i < n; i++ { + var v float32 + switch format { + case FormatFloat32LE: + v = math.Float32frombits(uint32(src[4*i]) | uint32(src[4*i+1])<<8 | uint32(src[4*i+2])<<16 | uint32(src[4*i+3])<<24) + case FormatUnsignedInt8: + v8 := src[i] + v = float32(v8-(1<<7)) / (1 << 7) + case FormatSignedInt16LE: + v16 := int16(src[2*i]) | (int16(src[2*i+1]) << 8) + v = float32(v16) / (1 << 15) + default: + panic(fmt.Sprintf("mux: unexpected format: %d", format)) + } + if volume == prevVolume { + buf[i] += v * volume + } else { + rate := float32(i/channelCount) / rateDenom + if rate > 1 { + rate = 1 + } + buf[i] += v * (volume*rate + prevVolume*(1-rate)) + } + } + + p.prevVolume = p.volume + + copy(p.buf, p.buf[n*bitDepthInBytes:]) + p.buf = p.buf[:len(p.buf)-n*bitDepthInBytes] + + if p.eof && len(p.buf) == 0 { + p.returnBufferToPool() + p.state = playerPaused + } + + return n +} + +func (p *playerImpl) canReadSourceToBuffer() bool { + p.m.Lock() + defer p.m.Unlock() + + if p.eof { + return false + } + return len(p.buf) < p.bufferSize +} + +func (p *playerImpl) readSourceToBuffer() int { + p.m.Lock() + defer p.m.Unlock() + + if p.err != nil { + return 0 + } + if p.state == playerClosed { + return 0 + } + + if len(p.buf) >= p.bufferSize { + return 0 + } + + buf := getBufferFromPool(p.bufferSize) + defer theBufPool.Put(buf) + n, err := p.read(*buf) + + if err != nil && err != io.EOF { + p.setErrorImpl(err) + return 0 + } + + if p.buf == nil { + p.buf = (*getBufferFromPool(p.bufferSize))[:0] + } + + p.buf = append(p.buf, (*buf)[:n]...) + if err == io.EOF { + p.eof = true + if len(p.buf) == 0 { + p.state = playerPaused + } + } + return n +} + +func (p *playerImpl) setErrorImpl(err error) { + p.err = err + p.closeImpl() +} + +func (p *playerImpl) returnBufferToPool() { + if p.buf != nil { + buf := p.buf + theBufPool.Put(&buf) + p.buf = nil + } +} + +// TODO: The term 'buffer' is confusing. Name each buffer with good terms. + +// defaultBufferSize returns the default size of the buffer for the audio source. +// This buffer is used when unreading on pausing the player. +func (m *Mux) defaultBufferSize() int { + bytesPerSample := m.channelCount * m.format.ByteLength() + s := m.sampleRate * bytesPerSample / 2 // 0.5[s] + // Align s in multiples of bytes per sample, or a buffer could have extra bytes. + return s / bytesPerSample * bytesPerSample +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/.clang-format b/vendor/github.com/ebitengine/oto/v3/internal/oboe/.clang-format new file mode 100644 index 0000000..9d15924 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/.clang-format @@ -0,0 +1,2 @@ +DisableFormat: true +SortIncludes: false diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/LICENSE-oboe b/vendor/github.com/ebitengine/oto/v3/internal/oboe/LICENSE-oboe new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/LICENSE-oboe @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/README-oboe.md b/vendor/github.com/ebitengine/oto/v3/internal/oboe/README-oboe.md new file mode 100644 index 0000000..3771338 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/README-oboe.md @@ -0,0 +1,54 @@ +# Oboe [![Build CI](https://github.com/google/oboe/workflows/Build%20CI/badge.svg)](https://github.com/google/oboe/actions) + +[![Introduction to Oboe video](docs/images/getting-started-video.jpg)](https://www.youtube.com/watch?v=csfHAbr5ilI&list=PLWz5rJ2EKKc_duWv9IPNvx9YBudNMmLSa) + +Oboe is a C++ library which makes it easy to build high-performance audio apps on Android. It was created primarily to allow developers to target a simplified API that works across multiple API levels back to API level 16 (Jelly Bean). + +## Features +- Compatible with API 16 onwards - runs on 99% of Android devices +- Chooses the audio API (OpenSL ES on API 16+ or AAudio on API 27+) which will give the best audio performance on the target Android device +- Automatic latency tuning +- Modern C++ allowing you to write clean, elegant code +- Workarounds for some known issues +- [Used by popular apps and frameworks](https://github.com/google/oboe/wiki/AppsUsingOboe) + +## Documentation +- [Getting Started Guide](docs/GettingStarted.md) +- [Full Guide to Oboe](docs/FullGuide.md) +- [API reference](https://google.github.io/oboe) +- [History of Audio features/bugs by Android version](docs/AndroidAudioHistory.md) +- [Migration guide for apps using OpenSL ES](docs/OpenSLESMigration.md) +- [Frequently Asked Questions](docs/FAQ.md) (FAQ) +- [Wiki](https://github.com/google/oboe/wiki) +- [Our roadmap](https://github.com/google/oboe/milestones) - Vote on a feature/issue by adding a thumbs up to the first comment. + +### Community +- Reddit: [r/androidaudiodev](https://www.reddit.com/r/androidaudiodev/) +- StackOverflow: [#oboe](https://stackoverflow.com/questions/tagged/oboe) + +## Testing +- [**OboeTester** app for measuring latency, glitches, etc.](apps/OboeTester/docs) +- [Oboe unit tests](tests) + +## Videos +- [Getting started with Oboe](https://www.youtube.com/playlist?list=PLWz5rJ2EKKc_duWv9IPNvx9YBudNMmLSa) +- [Low Latency Audio - Because Your Ears Are Worth It](https://www.youtube.com/watch?v=8vOf_fDtur4) (Android Dev Summit '18) +- [Winning on Android](https://www.youtube.com/watch?v=tWBojmBpS74) - How to optimize an Android audio app. (ADC '18) + +## Sample code and apps +- Sample apps can be found in the [samples directory](samples). +- A complete "effects processor" app called FXLab can be found in the [apps/fxlab folder](apps/fxlab). +- Also check out the [Rhythm Game codelab](https://developer.android.com/codelabs/musicalgame-using-oboe?hl=en#0). + +### Third party sample code +- [Ableton Link integration demo](https://github.com/jbloit/AndroidLinkAudio) (author: jbloit) + +## Contributing +We would love to receive your pull requests. Before we can though, please read the [contributing](CONTRIBUTING.md) guidelines. + +## Version history +View the [releases page](../../releases). + +## License +[LICENSE](LICENSE) + diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/binding_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/binding_android.cpp new file mode 100644 index 0000000..48b9699 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/binding_android.cpp @@ -0,0 +1,191 @@ +// Copyright 2021 The Oto Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "binding_android.h" + +#include "_cgo_export.h" +#include "oboe_oboe_Oboe_android.h" + +#include +#include +#include +#include +#include + +namespace { + +class Player; + +class Stream : public oboe::AudioStreamDataCallback { +public: + // GetInstance returns the instance of Stream. Only one Stream object is used + // in one process. It is because multiple streams can be problematic in both + // AAudio and OpenSL (#1656, #1660). + static Stream &GetInstance(); + + const char *Play(int sample_rate, int channel_num, int buffer_size_in_bytes); + const char *Pause(); + const char *Resume(); + const char *Close(); + const char *AppendBuffer(float *buf, size_t len); + + oboe::DataCallbackResult onAudioReady(oboe::AudioStream *oboe_stream, + void *audio_data, + int32_t num_frames) override; + +private: + Stream(); + void Loop(int num_frames); + + int sample_rate_ = 0; + int channel_num_ = 0; + + std::shared_ptr stream_; + + // All the member variables other than the thread must be initialized before + // the thread. + std::vector buf_; + std::mutex mutex_; + std::condition_variable cond_; + std::unique_ptr thread_; +}; + +Stream &Stream::GetInstance() { + static Stream *stream = new Stream(); + return *stream; +} + +const char *Stream::Play(int sample_rate, int channel_num, + int buffer_size_in_bytes) { + sample_rate_ = sample_rate; + channel_num_ = channel_num; + + if (!stream_) { + oboe::AudioStreamBuilder builder; + builder.setDirection(oboe::Direction::Output) + ->setPerformanceMode(oboe::PerformanceMode::LowLatency) + ->setSharingMode(oboe::SharingMode::Shared) + ->setFormat(oboe::AudioFormat::Float) + ->setChannelCount(channel_num_) + ->setSampleRate(sample_rate_) + ->setDataCallback(this); + if (buffer_size_in_bytes) { + int buffer_size_in_frames = buffer_size_in_bytes / channel_num / 4; + builder.setBufferCapacityInFrames(buffer_size_in_frames); + } + oboe::Result result = builder.openStream(stream_); + if (result != oboe::Result::OK) { + return oboe::convertToText(result); + } + } + if (stream_->getSharingMode() != oboe::SharingMode::Shared) { + return "oboe::SharingMode::Shared is not available"; + } + + int num_frames = stream_->getBufferSizeInFrames(); + thread_ = + std::make_unique([this, num_frames]() { Loop(num_frames); }); + + // What if the buffer size is not enough? + if (oboe::Result result = stream_->start(); result != oboe::Result::OK) { + return oboe::convertToText(result); + } + return nullptr; +} + +const char *Stream::Pause() { + if (!stream_) { + return nullptr; + } + if (oboe::Result result = stream_->pause(); result != oboe::Result::OK) { + return oboe::convertToText(result); + } + return nullptr; +} + +const char *Stream::Resume() { + if (!stream_) { + return "Play is not called yet at Resume"; + } + if (oboe::Result result = stream_->start(); result != oboe::Result::OK) { + return oboe::convertToText(result); + } + return nullptr; +} + +const char *Stream::Close() { + // Nobody calls this so far. + if (!stream_) { + return nullptr; + } + if (oboe::Result result = stream_->stop(); result != oboe::Result::OK) { + return oboe::convertToText(result); + } + if (oboe::Result result = stream_->close(); result != oboe::Result::OK) { + return oboe::convertToText(result); + } + stream_.reset(); + return nullptr; +} + +oboe::DataCallbackResult Stream::onAudioReady(oboe::AudioStream *oboe_stream, + void *audio_data, + int32_t num_frames) { + size_t num = num_frames * channel_num_; + // TODO: Do not use a lock in onAudioReady. + // https://google.github.io/oboe/reference/classoboe_1_1_audio_stream_data_callback.html#ad8a3a9f609df5fd3a5d885cbe1b2204d + { + std::unique_lock lock{mutex_}; + cond_.wait(lock, [this, num] { return buf_.size() >= num; }); + std::copy(buf_.begin(), buf_.begin() + num, + reinterpret_cast(audio_data)); + buf_.erase(buf_.begin(), buf_.begin() + num); + cond_.notify_one(); + } + return oboe::DataCallbackResult::Continue; +} + +Stream::Stream() = default; + +void Stream::Loop(int num_frames) { + std::vector tmp(num_frames * channel_num_ * 3); + for (;;) { + { + std::unique_lock lock{mutex_}; + cond_.wait(lock, [this, &tmp] { return buf_.size() < tmp.size(); }); + } + oto_oboe_read(&tmp[0], tmp.size()); + { + std::lock_guard lock{mutex_}; + buf_.insert(buf_.end(), tmp.begin(), tmp.end()); + cond_.notify_one(); + } + } +} + +} // namespace + +extern "C" { + +const char *oto_oboe_Play(int sample_rate, int channel_num, + int buffer_size_in_bytes) { + return Stream::GetInstance().Play(sample_rate, channel_num, + buffer_size_in_bytes); +} + +const char *oto_oboe_Suspend() { return Stream::GetInstance().Pause(); } + +const char *oto_oboe_Resume() { return Stream::GetInstance().Resume(); } + +} // extern "C" diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/binding_android.go b/vendor/github.com/ebitengine/oto/v3/internal/oboe/binding_android.go new file mode 100644 index 0000000..0fceb71 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/binding_android.go @@ -0,0 +1,60 @@ +// Copyright 2021 The Oto Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package oboe + +// Disable AAudio (hajimehoshi/ebiten#1634). +// AAudio doesn't care about plugging in/out of a headphone. +// See https://github.com/google/oboe/wiki/TechNote_Disconnect + +// #cgo CXXFLAGS: -std=c++17 -DOBOE_ENABLE_AAUDIO=0 +// #cgo LDFLAGS: -llog -lOpenSLES -static-libstdc++ +// +// #include "binding_android.h" +import "C" + +import ( + "fmt" + "unsafe" +) + +var theReadFunc func(buf []float32) + +func Play(sampleRate int, channelCount int, readFunc func(buf []float32), bufferSizeInBytes int) error { + // Play can invoke the callback. Set the callback before Play. + theReadFunc = readFunc + if msg := C.oto_oboe_Play(C.int(sampleRate), C.int(channelCount), C.int(bufferSizeInBytes)); msg != nil { + return fmt.Errorf("oboe: Play failed: %s", C.GoString(msg)) + } + return nil +} + +func Suspend() error { + if msg := C.oto_oboe_Suspend(); msg != nil { + return fmt.Errorf("oboe: Suspend failed: %s", C.GoString(msg)) + } + return nil +} + +func Resume() error { + if msg := C.oto_oboe_Resume(); msg != nil { + return fmt.Errorf("oboe: Resume failed: %s", C.GoString(msg)) + } + return nil +} + +//export oto_oboe_read +func oto_oboe_read(buf *C.float, len C.size_t) { + theReadFunc(unsafe.Slice((*float32)(unsafe.Pointer(buf)), len)) +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/binding_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/binding_android.h new file mode 100644 index 0000000..82c5c07 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/binding_android.h @@ -0,0 +1,37 @@ +// Copyright 2021 The Oto Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef OBOE_ANDROID_H_ +#define OBOE_ANDROID_H_ + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef uintptr_t PlayerID; + +const char *oto_oboe_Play(int sample_rate, int channel_num, + int buffer_size_in_bytes); +const char *oto_oboe_Suspend(); +const char *oto_oboe_Resume(); + +#ifdef __cplusplus +} +#endif + +#endif // OBOE_ANDROID_H_ diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/generate.go b/vendor/github.com/ebitengine/oto/v3/internal/oboe/generate.go new file mode 100644 index 0000000..0f6dbe1 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/generate.go @@ -0,0 +1,17 @@ +// Copyright 2021 The Oto Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:generate go run gen.go + +package oboe diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_aaudio_AAudioExtensions_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_aaudio_AAudioExtensions_android.h new file mode 100644 index 0000000..cea5a48 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_aaudio_AAudioExtensions_android.h @@ -0,0 +1,309 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_AAUDIO_EXTENSIONS_H +#define OBOE_AAUDIO_EXTENSIONS_H + +#include +#include +#include +#include + +#include + +#include "oboe_common_OboeDebug_android.h" +#include "oboe_oboe_Oboe_android.h" +#include "oboe_aaudio_AAudioLoader_android.h" + +namespace oboe { + +#define LIB_AAUDIO_NAME "libaaudio.so" +#define FUNCTION_IS_MMAP "AAudioStream_isMMapUsed" +#define FUNCTION_SET_MMAP_POLICY "AAudio_setMMapPolicy" +#define FUNCTION_GET_MMAP_POLICY "AAudio_getMMapPolicy" + +#define AAUDIO_ERROR_UNAVAILABLE static_cast(Result::ErrorUnavailable) + +typedef struct AAudioStreamStruct AAudioStream; + +// The output device type collection must be updated if there is any new added output device type +const static std::set ALL_OUTPUT_DEVICE_TYPES = { + DeviceType::BuiltinEarpiece, + DeviceType::BuiltinSpeaker, + DeviceType::WiredHeadset, + DeviceType::WiredHeadphones, + DeviceType::LineAnalog, + DeviceType::LineDigital, + DeviceType::BluetoothSco, + DeviceType::BluetoothA2dp, + DeviceType::Hdmi, + DeviceType::HdmiArc, + DeviceType::HdmiEarc, + DeviceType::UsbDevice, + DeviceType::UsbHeadset, + DeviceType::UsbAccessory, + DeviceType::Dock, + DeviceType::DockAnalog, + DeviceType::FM, + DeviceType::Telephony, + DeviceType::AuxLine, + DeviceType::IP, + DeviceType::Bus, + DeviceType::HearingAid, + DeviceType::BuiltinSpeakerSafe, + DeviceType::RemoteSubmix, + DeviceType::BleHeadset, + DeviceType::BleSpeaker, + DeviceType::BleBroadcast, +}; + +// The input device type collection must be updated if there is any new added input device type +const static std::set ALL_INPUT_DEVICE_TYPES = { + DeviceType::BuiltinMic, + DeviceType::BluetoothSco, + DeviceType::WiredHeadset, + DeviceType::Hdmi, + DeviceType::Telephony, + DeviceType::Dock, + DeviceType::DockAnalog, + DeviceType::UsbAccessory, + DeviceType::UsbDevice, + DeviceType::UsbHeadset, + DeviceType::FMTuner, + DeviceType::TVTuner, + DeviceType::LineAnalog, + DeviceType::LineDigital, + DeviceType::BluetoothA2dp, + DeviceType::IP, + DeviceType::Bus, + DeviceType::RemoteSubmix, + DeviceType::BleHeadset, + DeviceType::HdmiArc, + DeviceType::HdmiEarc, +}; + +/** + * Call some AAudio test routines that are not part of the normal API. + */ +class AAudioExtensions { +private: // Because it is a singleton. Call getInstance() instead. + AAudioExtensions() { + mLibLoader = AAudioLoader::getInstance(); + if (!initMMapPolicy()) { + int32_t policy = getIntegerProperty("aaudio.mmap_policy", 0); + mMMapSupported = isPolicyEnabled(policy); + + policy = getIntegerProperty("aaudio.mmap_exclusive_policy", 0); + mMMapExclusiveSupported = isPolicyEnabled(policy); + } + } + +public: + static bool isPolicyEnabled(int32_t policy) { + const MMapPolicy mmapPolicy = static_cast(policy); + return (mmapPolicy == MMapPolicy::Auto || mmapPolicy == MMapPolicy::Always); + } + + static AAudioExtensions &getInstance() { + static AAudioExtensions instance; + return instance; + } + + bool isMMapUsed(oboe::AudioStream *oboeStream) { + AAudioStream *aaudioStream = (AAudioStream *) oboeStream->getUnderlyingStream(); + return isMMapUsed(aaudioStream); + } + + bool isMMapUsed(AAudioStream *aaudioStream) { + if (mLibLoader != nullptr && mLibLoader->stream_isMMapUsed != nullptr) { + return mLibLoader->stream_isMMapUsed(aaudioStream); + } + if (loadSymbols()) return false; + if (mAAudioStream_isMMap == nullptr) return false; + return mAAudioStream_isMMap(aaudioStream); + } + + /** + * Controls whether the MMAP data path can be selected when opening a stream. + * It has no effect after the stream has been opened. + * It only affects the application that calls it. Other apps are not affected. + * + * @param enabled + * @return 0 or a negative error code + */ + int32_t setMMapEnabled(bool enabled) { + // The API for setting mmap policy is public after API level 36. + if (mLibLoader != nullptr && mLibLoader->aaudio_setMMapPolicy != nullptr) { + return mLibLoader->aaudio_setMMapPolicy( + static_cast(enabled ? MMapPolicy::Auto : MMapPolicy::Never)); + } + // When there is no public API, fallback to loading the symbol from hidden API. + if (loadSymbols()) return AAUDIO_ERROR_UNAVAILABLE; + if (mAAudio_setMMapPolicy == nullptr) return false; + return mAAudio_setMMapPolicy( + static_cast(enabled ? MMapPolicy::Auto : MMapPolicy::Never)); + } + + bool isMMapEnabled() { + // The API for getting mmap policy is public after API level 36. + // Use it when it is available. + if (mLibLoader != nullptr && mLibLoader->aaudio_getMMapPolicy != nullptr) { + MMapPolicy policy = static_cast(mLibLoader->aaudio_getMMapPolicy()); + return policy == MMapPolicy::Unspecified + ? mMMapSupported : isPolicyEnabled(static_cast(policy)); + } + // When there is no public API, fallback to loading the symbol from hidden API. + if (loadSymbols()) return false; + if (mAAudio_getMMapPolicy == nullptr) return false; + int32_t policy = mAAudio_getMMapPolicy(); + return (policy == Unspecified) ? mMMapSupported : isPolicyEnabled(policy); + } + + bool isMMapSupported() { + return mMMapSupported; + } + + bool isMMapExclusiveSupported() { + return mMMapExclusiveSupported; + } + + MMapPolicy getMMapPolicy(DeviceType deviceType, Direction direction) { + if (mLibLoader == nullptr || + mLibLoader->aaudio_getPlatformMMapPolicy == nullptr) { + return MMapPolicy::Unspecified; + } + return static_cast(mLibLoader->aaudio_getPlatformMMapPolicy( + static_cast(deviceType), + static_cast(direction))); + } + + MMapPolicy getMMapExclusivePolicy(DeviceType deviceType, Direction direction) { + if (mLibLoader == nullptr || + mLibLoader->aaudio_getPlatformMMapExclusivePolicy == nullptr) { + return MMapPolicy::Unspecified; + } + return static_cast(mLibLoader->aaudio_getPlatformMMapExclusivePolicy( + static_cast(deviceType), + static_cast(direction))); + } + + bool isPartialDataCallbackSupported() { + return mLibLoader != nullptr && mLibLoader->builder_setPartialDataCallback != nullptr; + } + +private: + bool initMMapPolicy() { + if (mLibLoader == nullptr || mLibLoader->open() != 0) { + return false; + } + if (mLibLoader->aaudio_getPlatformMMapPolicy == nullptr || + mLibLoader->aaudio_getPlatformMMapExclusivePolicy == nullptr) { + return false; + } + mMMapSupported = + std::any_of(ALL_INPUT_DEVICE_TYPES.begin(), ALL_INPUT_DEVICE_TYPES.end(), + [this](DeviceType deviceType) { + return isPolicyEnabled(static_cast( + getMMapPolicy(deviceType, Direction::Input))); + }) || + std::any_of(ALL_OUTPUT_DEVICE_TYPES.begin(), ALL_OUTPUT_DEVICE_TYPES.end(), + [this](DeviceType deviceType) { + return isPolicyEnabled(static_cast( + getMMapPolicy(deviceType, Direction::Output))); + }); + mMMapExclusiveSupported = + std::any_of(ALL_INPUT_DEVICE_TYPES.begin(), ALL_INPUT_DEVICE_TYPES.end(), + [this](DeviceType deviceType) { + return isPolicyEnabled(static_cast( + getMMapExclusivePolicy(deviceType, Direction::Input))); + }) || + std::any_of(ALL_OUTPUT_DEVICE_TYPES.begin(), ALL_OUTPUT_DEVICE_TYPES.end(), + [this](DeviceType deviceType) { + return isPolicyEnabled(static_cast( + getMMapExclusivePolicy(deviceType, Direction::Output))); + }); + return true; + } + + int getIntegerProperty(const char *name, int defaultValue) { + int result = defaultValue; + char valueText[PROP_VALUE_MAX] = {0}; + if (__system_property_get(name, valueText) != 0) { + result = atoi(valueText); + } + return result; + } + + /** + * Load the function pointers. + * This can be called multiple times. + * It should only be called from one thread. + * + * @return 0 if successful or negative error. + */ + aaudio_result_t loadSymbols() { + if (mAAudio_getMMapPolicy != nullptr) { + return 0; + } + + if (mLibLoader == nullptr || mLibLoader->open() != 0) { + LOGD("%s() could not open " LIB_AAUDIO_NAME, __func__); + return AAUDIO_ERROR_UNAVAILABLE; + } + + void *libHandle = mLibLoader->getLibHandle(); + if (libHandle == nullptr) { + LOGE("%s() could not find " LIB_AAUDIO_NAME, __func__); + return AAUDIO_ERROR_UNAVAILABLE; + } + + mAAudioStream_isMMap = (bool (*)(AAudioStream *stream)) + dlsym(libHandle, FUNCTION_IS_MMAP); + if (mAAudioStream_isMMap == nullptr) { + LOGI("%s() could not find " FUNCTION_IS_MMAP, __func__); + return AAUDIO_ERROR_UNAVAILABLE; + } + + mAAudio_setMMapPolicy = (int32_t (*)(aaudio_policy_t policy)) + dlsym(libHandle, FUNCTION_SET_MMAP_POLICY); + if (mAAudio_setMMapPolicy == nullptr) { + LOGI("%s() could not find " FUNCTION_SET_MMAP_POLICY, __func__); + return AAUDIO_ERROR_UNAVAILABLE; + } + + mAAudio_getMMapPolicy = (aaudio_policy_t (*)()) + dlsym(libHandle, FUNCTION_GET_MMAP_POLICY); + if (mAAudio_getMMapPolicy == nullptr) { + LOGI("%s() could not find " FUNCTION_GET_MMAP_POLICY, __func__); + return AAUDIO_ERROR_UNAVAILABLE; + } + + return 0; + } + + bool mMMapSupported = false; + bool mMMapExclusiveSupported = false; + + bool (*mAAudioStream_isMMap)(AAudioStream *stream) = nullptr; + int32_t (*mAAudio_setMMapPolicy)(aaudio_policy_t policy) = nullptr; + aaudio_policy_t (*mAAudio_getMMapPolicy)() = nullptr; + + AAudioLoader *mLibLoader; +}; + +} // namespace oboe + +#endif //OBOE_AAUDIO_EXTENSIONS_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_aaudio_AAudioLoader_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_aaudio_AAudioLoader_android.cpp new file mode 100644 index 0000000..6e3f401 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_aaudio_AAudioLoader_android.cpp @@ -0,0 +1,634 @@ +/* + * Copyright 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "oboe_oboe_Utilities_android.h" +#include "oboe_common_OboeDebug_android.h" +#include "oboe_aaudio_AAudioLoader_android.h" + +#define LIB_AAUDIO_NAME "libaaudio.so" + +namespace oboe { + +AAudioLoader::~AAudioLoader() { + // Issue 360: thread_local variables with non-trivial destructors + // will cause segfaults if the containing library is dlclose()ed on + // devices running M or newer, or devices before M when using a static STL. + // The simple workaround is to not call dlclose. + // https://github.com/android/ndk/wiki/Changelog-r22#known-issues + // + // The libaaudio and libaaudioclient do not use thread_local. + // But, to be safe, we should avoid dlclose() if possible. + // Because AAudioLoader is a static Singleton, we can safely skip + // calling dlclose() without causing a resource leak. + LOGI("%s() dlclose(%s) not called, OK", __func__, LIB_AAUDIO_NAME); +} + +AAudioLoader* AAudioLoader::getInstance() { + static AAudioLoader instance; + return &instance; +} + +int AAudioLoader::open() { + if (mLibHandle != nullptr) { + return 0; + } + + // Use RTLD_NOW to avoid the unpredictable behavior that RTLD_LAZY can cause. + // Also resolving all the links now will prevent a run-time penalty later. + mLibHandle = dlopen(LIB_AAUDIO_NAME, RTLD_NOW); + if (mLibHandle == nullptr) { + LOGI("AAudioLoader::open() could not find " LIB_AAUDIO_NAME); + return -1; // TODO review return code + } else { + LOGD("AAudioLoader(): dlopen(%s) returned %p", LIB_AAUDIO_NAME, mLibHandle); + } + + // Load all the function pointers. + createStreamBuilder = load_I_PPB("AAudio_createStreamBuilder"); + builder_openStream = load_I_PBPPS("AAudioStreamBuilder_openStream"); + + builder_setChannelCount = load_V_PBI("AAudioStreamBuilder_setChannelCount"); + if (builder_setChannelCount == nullptr) { + // Use old deprecated alias if needed. + builder_setChannelCount = load_V_PBI("AAudioStreamBuilder_setSamplesPerFrame"); + } + + builder_setBufferCapacityInFrames = load_V_PBI("AAudioStreamBuilder_setBufferCapacityInFrames"); + builder_setDeviceId = load_V_PBI("AAudioStreamBuilder_setDeviceId"); + builder_setDirection = load_V_PBI("AAudioStreamBuilder_setDirection"); + builder_setFormat = load_V_PBI("AAudioStreamBuilder_setFormat"); + builder_setFramesPerDataCallback = load_V_PBI("AAudioStreamBuilder_setFramesPerDataCallback"); + builder_setSharingMode = load_V_PBI("AAudioStreamBuilder_setSharingMode"); + builder_setPerformanceMode = load_V_PBI("AAudioStreamBuilder_setPerformanceMode"); + builder_setSampleRate = load_V_PBI("AAudioStreamBuilder_setSampleRate"); + + if (getSdkVersion() >= __ANDROID_API_P__){ + builder_setUsage = load_V_PBI("AAudioStreamBuilder_setUsage"); + builder_setContentType = load_V_PBI("AAudioStreamBuilder_setContentType"); + builder_setInputPreset = load_V_PBI("AAudioStreamBuilder_setInputPreset"); + builder_setSessionId = load_V_PBI("AAudioStreamBuilder_setSessionId"); + } + + if (getSdkVersion() >= __ANDROID_API_Q__){ + builder_setAllowedCapturePolicy = load_V_PBI("AAudioStreamBuilder_setAllowedCapturePolicy"); + } + + if (getSdkVersion() >= __ANDROID_API_R__){ + builder_setPrivacySensitive = load_V_PBO("AAudioStreamBuilder_setPrivacySensitive"); + } + + if (getSdkVersion() >= __ANDROID_API_S__){ + builder_setPackageName = load_V_PBCPH("AAudioStreamBuilder_setPackageName"); + builder_setAttributionTag = load_V_PBCPH("AAudioStreamBuilder_setAttributionTag"); + } + + if (getSdkVersion() >= __ANDROID_API_S_V2__) { + builder_setChannelMask = load_V_PBU("AAudioStreamBuilder_setChannelMask"); + builder_setIsContentSpatialized = load_V_PBO("AAudioStreamBuilder_setIsContentSpatialized"); + builder_setSpatializationBehavior = load_V_PBI("AAudioStreamBuilder_setSpatializationBehavior"); + } + + if (getSdkVersion() >= __ANDROID_API_B__) { + builder_setPresentationEndCallback = load_V_PBPRPV("AAudioStreamBuilder_setPresentationEndCallback"); + } + + builder_delete = load_I_PB("AAudioStreamBuilder_delete"); + + + builder_setDataCallback = load_V_PBPDPV("AAudioStreamBuilder_setDataCallback"); + builder_setErrorCallback = load_V_PBPEPV("AAudioStreamBuilder_setErrorCallback"); + + stream_read = load_I_PSPVIL("AAudioStream_read"); + + stream_write = load_I_PSCPVIL("AAudioStream_write"); + + stream_waitForStateChange = load_I_PSTPTL("AAudioStream_waitForStateChange"); + + stream_getTimestamp = load_I_PSKPLPL("AAudioStream_getTimestamp"); + + stream_getChannelCount = load_I_PS("AAudioStream_getChannelCount"); + if (stream_getChannelCount == nullptr) { + // Use old alias if needed. + stream_getChannelCount = load_I_PS("AAudioStream_getSamplesPerFrame"); + } + + if (getSdkVersion() >= __ANDROID_API_R__) { + stream_release = load_I_PS("AAudioStream_release"); + } + + stream_close = load_I_PS("AAudioStream_close"); + + stream_getBufferSize = load_I_PS("AAudioStream_getBufferSizeInFrames"); + stream_getDeviceId = load_I_PS("AAudioStream_getDeviceId"); + stream_getBufferCapacity = load_I_PS("AAudioStream_getBufferCapacityInFrames"); + stream_getFormat = load_F_PS("AAudioStream_getFormat"); + stream_getFramesPerBurst = load_I_PS("AAudioStream_getFramesPerBurst"); + stream_getFramesRead = load_L_PS("AAudioStream_getFramesRead"); + stream_getFramesWritten = load_L_PS("AAudioStream_getFramesWritten"); + stream_getPerformanceMode = load_I_PS("AAudioStream_getPerformanceMode"); + stream_getSampleRate = load_I_PS("AAudioStream_getSampleRate"); + stream_getSharingMode = load_I_PS("AAudioStream_getSharingMode"); + stream_getState = load_I_PS("AAudioStream_getState"); + stream_getXRunCount = load_I_PS("AAudioStream_getXRunCount"); + + stream_requestStart = load_I_PS("AAudioStream_requestStart"); + stream_requestPause = load_I_PS("AAudioStream_requestPause"); + stream_requestFlush = load_I_PS("AAudioStream_requestFlush"); + stream_requestStop = load_I_PS("AAudioStream_requestStop"); + + stream_setBufferSize = load_I_PSI("AAudioStream_setBufferSizeInFrames"); + + convertResultToText = load_CPH_I("AAudio_convertResultToText"); + + if (getSdkVersion() >= __ANDROID_API_P__){ + stream_getUsage = load_I_PS("AAudioStream_getUsage"); + stream_getContentType = load_I_PS("AAudioStream_getContentType"); + stream_getInputPreset = load_I_PS("AAudioStream_getInputPreset"); + stream_getSessionId = load_I_PS("AAudioStream_getSessionId"); + } + + if (getSdkVersion() >= __ANDROID_API_Q__){ + stream_getAllowedCapturePolicy = load_I_PS("AAudioStream_getAllowedCapturePolicy"); + } + + if (getSdkVersion() >= __ANDROID_API_R__){ + stream_isPrivacySensitive = load_O_PS("AAudioStream_isPrivacySensitive"); + } + + if (getSdkVersion() >= __ANDROID_API_S_V2__) { + stream_getChannelMask = load_U_PS("AAudioStream_getChannelMask"); + stream_isContentSpatialized = load_O_PS("AAudioStream_isContentSpatialized"); + stream_getSpatializationBehavior = load_I_PS("AAudioStream_getSpatializationBehavior"); + } + + if (getSdkVersion() >= __ANDROID_API_U__) { + stream_getHardwareChannelCount = load_I_PS("AAudioStream_getHardwareChannelCount"); + stream_getHardwareSampleRate = load_I_PS("AAudioStream_getHardwareSampleRate"); + stream_getHardwareFormat = load_F_PS("AAudioStream_getHardwareFormat"); + } + + // TODO: Remove pre-release check after Android B release + if (getSdkVersion() >= __ANDROID_API_B__ || isAtLeastPreReleaseCodename("Baklava")) { + aaudio_getPlatformMMapPolicy = load_I_II("AAudio_getPlatformMMapPolicy"); + aaudio_getPlatformMMapExclusivePolicy = load_I_II("AAudio_getPlatformMMapExclusivePolicy"); + aaudio_setMMapPolicy = load_I_I("AAudio_setMMapPolicy"); + aaudio_getMMapPolicy = load_I("AAudio_getMMapPolicy"); + stream_isMMapUsed = load_O_PS("AAudioStream_isMMapUsed"); + + stream_setOffloadDelayPadding = load_I_PSII("AAudioStream_setOffloadDelayPadding"); + stream_getOffloadDelay = load_I_PS("AAudioStream_getOffloadDelay"); + stream_getOffloadPadding = load_I_PS("AAudioStream_getOffloadPadding"); + stream_setOffloadEndOfStream = load_I_PS("AAudioStream_setOffloadEndOfStream"); + + stream_getDeviceIds = load_I_PSPIPI("AAudioStream_getDeviceIds"); + + // TODO: Use 25Q4 version code and name when it is defined. + stream_flushFromFrame = load_I_PSIPL("AAudioStream_flushFromFrame"); + stream_getPlaybackParameters = + load_I_PSPM("AAudioStream_getPlaybackParameters"); + stream_setPlaybackParameters = + load_I_PSCPM("AAudioStream_setPlaybackParameters"); + + builder_setPartialDataCallback = + load_V_PBPDPV("AAudioStreamBuilder_setPartialDataCallback"); + } + + return 0; +} + +static void AAudioLoader_check(void *proc, const char *functionName) { + if (proc == nullptr) { + LOGW("AAudioLoader could not find %s", functionName); + } +} + +AAudioLoader::signature_I_PPB AAudioLoader::load_I_PPB(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_CPH_I AAudioLoader::load_CPH_I(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_V_PBI AAudioLoader::load_V_PBI(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_V_PBCPH AAudioLoader::load_V_PBCPH(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_V_PBPDPV AAudioLoader::load_V_PBPDPV(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_V_PBPEPV AAudioLoader::load_V_PBPEPV(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_I_PSI AAudioLoader::load_I_PSI(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_I_PS AAudioLoader::load_I_PS(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_L_PS AAudioLoader::load_L_PS(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_F_PS AAudioLoader::load_F_PS(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_O_PS AAudioLoader::load_O_PS(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_I_PB AAudioLoader::load_I_PB(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_I_PBPPS AAudioLoader::load_I_PBPPS(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_I_PSCPVIL AAudioLoader::load_I_PSCPVIL(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_I_PSPVIL AAudioLoader::load_I_PSPVIL(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_I_PSTPTL AAudioLoader::load_I_PSTPTL(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_I_PSKPLPL AAudioLoader::load_I_PSKPLPL(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_V_PBU AAudioLoader::load_V_PBU(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_U_PS AAudioLoader::load_U_PS(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_V_PBO AAudioLoader::load_V_PBO(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_I_II AAudioLoader::load_I_II(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_I_I AAudioLoader::load_I_I(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_I AAudioLoader::load_I(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_V_PBPRPV AAudioLoader::load_V_PBPRPV(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_I_PSII AAudioLoader::load_I_PSII(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_I_PSPIPI AAudioLoader::load_I_PSPIPI(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_I_PSIPL AAudioLoader::load_I_PSIPL(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_I_PSPM AAudioLoader::load_I_PSPM(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_I_PSCPM AAudioLoader::load_I_PSCPM(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +// Ensure that all AAudio primitive data types are int32_t +#define ASSERT_INT32(type) static_assert(std::is_same::value, \ +#type" must be int32_t") + +// Ensure that all AAudio primitive data types are uint32_t +#define ASSERT_UINT32(type) static_assert(std::is_same::value, \ +#type" must be uint32_t") + +#define ERRMSG "Oboe constants must match AAudio constants." + +// These asserts help verify that the Oboe definitions match the equivalent AAudio definitions. +// This code is in this .cpp file so it only gets tested once. +#ifdef AAUDIO_AAUDIO_H + + ASSERT_INT32(aaudio_stream_state_t); + ASSERT_INT32(aaudio_direction_t); + ASSERT_INT32(aaudio_format_t); + ASSERT_INT32(aaudio_data_callback_result_t); + ASSERT_INT32(aaudio_result_t); + ASSERT_INT32(aaudio_sharing_mode_t); + ASSERT_INT32(aaudio_performance_mode_t); + + static_assert((int32_t)StreamState::Uninitialized == AAUDIO_STREAM_STATE_UNINITIALIZED, ERRMSG); + static_assert((int32_t)StreamState::Unknown == AAUDIO_STREAM_STATE_UNKNOWN, ERRMSG); + static_assert((int32_t)StreamState::Open == AAUDIO_STREAM_STATE_OPEN, ERRMSG); + static_assert((int32_t)StreamState::Starting == AAUDIO_STREAM_STATE_STARTING, ERRMSG); + static_assert((int32_t)StreamState::Started == AAUDIO_STREAM_STATE_STARTED, ERRMSG); + static_assert((int32_t)StreamState::Pausing == AAUDIO_STREAM_STATE_PAUSING, ERRMSG); + static_assert((int32_t)StreamState::Paused == AAUDIO_STREAM_STATE_PAUSED, ERRMSG); + static_assert((int32_t)StreamState::Flushing == AAUDIO_STREAM_STATE_FLUSHING, ERRMSG); + static_assert((int32_t)StreamState::Flushed == AAUDIO_STREAM_STATE_FLUSHED, ERRMSG); + static_assert((int32_t)StreamState::Stopping == AAUDIO_STREAM_STATE_STOPPING, ERRMSG); + static_assert((int32_t)StreamState::Stopped == AAUDIO_STREAM_STATE_STOPPED, ERRMSG); + static_assert((int32_t)StreamState::Closing == AAUDIO_STREAM_STATE_CLOSING, ERRMSG); + static_assert((int32_t)StreamState::Closed == AAUDIO_STREAM_STATE_CLOSED, ERRMSG); + static_assert((int32_t)StreamState::Disconnected == AAUDIO_STREAM_STATE_DISCONNECTED, ERRMSG); + + static_assert((int32_t)Direction::Output == AAUDIO_DIRECTION_OUTPUT, ERRMSG); + static_assert((int32_t)Direction::Input == AAUDIO_DIRECTION_INPUT, ERRMSG); + + static_assert((int32_t)AudioFormat::Invalid == AAUDIO_FORMAT_INVALID, ERRMSG); + static_assert((int32_t)AudioFormat::Unspecified == AAUDIO_FORMAT_UNSPECIFIED, ERRMSG); + static_assert((int32_t)AudioFormat::I16 == AAUDIO_FORMAT_PCM_I16, ERRMSG); + static_assert((int32_t)AudioFormat::Float == AAUDIO_FORMAT_PCM_FLOAT, ERRMSG); + + static_assert((int32_t)DataCallbackResult::Continue == AAUDIO_CALLBACK_RESULT_CONTINUE, ERRMSG); + static_assert((int32_t)DataCallbackResult::Stop == AAUDIO_CALLBACK_RESULT_STOP, ERRMSG); + + static_assert((int32_t)Result::OK == AAUDIO_OK, ERRMSG); + static_assert((int32_t)Result::ErrorBase == AAUDIO_ERROR_BASE, ERRMSG); + static_assert((int32_t)Result::ErrorDisconnected == AAUDIO_ERROR_DISCONNECTED, ERRMSG); + static_assert((int32_t)Result::ErrorIllegalArgument == AAUDIO_ERROR_ILLEGAL_ARGUMENT, ERRMSG); + static_assert((int32_t)Result::ErrorInternal == AAUDIO_ERROR_INTERNAL, ERRMSG); + static_assert((int32_t)Result::ErrorInvalidState == AAUDIO_ERROR_INVALID_STATE, ERRMSG); + static_assert((int32_t)Result::ErrorInvalidHandle == AAUDIO_ERROR_INVALID_HANDLE, ERRMSG); + static_assert((int32_t)Result::ErrorUnimplemented == AAUDIO_ERROR_UNIMPLEMENTED, ERRMSG); + static_assert((int32_t)Result::ErrorUnavailable == AAUDIO_ERROR_UNAVAILABLE, ERRMSG); + static_assert((int32_t)Result::ErrorNoFreeHandles == AAUDIO_ERROR_NO_FREE_HANDLES, ERRMSG); + static_assert((int32_t)Result::ErrorNoMemory == AAUDIO_ERROR_NO_MEMORY, ERRMSG); + static_assert((int32_t)Result::ErrorNull == AAUDIO_ERROR_NULL, ERRMSG); + static_assert((int32_t)Result::ErrorTimeout == AAUDIO_ERROR_TIMEOUT, ERRMSG); + static_assert((int32_t)Result::ErrorWouldBlock == AAUDIO_ERROR_WOULD_BLOCK, ERRMSG); + static_assert((int32_t)Result::ErrorInvalidFormat == AAUDIO_ERROR_INVALID_FORMAT, ERRMSG); + static_assert((int32_t)Result::ErrorOutOfRange == AAUDIO_ERROR_OUT_OF_RANGE, ERRMSG); + static_assert((int32_t)Result::ErrorNoService == AAUDIO_ERROR_NO_SERVICE, ERRMSG); + static_assert((int32_t)Result::ErrorInvalidRate == AAUDIO_ERROR_INVALID_RATE, ERRMSG); + + static_assert((int32_t)SharingMode::Exclusive == AAUDIO_SHARING_MODE_EXCLUSIVE, ERRMSG); + static_assert((int32_t)SharingMode::Shared == AAUDIO_SHARING_MODE_SHARED, ERRMSG); + + static_assert((int32_t)PerformanceMode::None == AAUDIO_PERFORMANCE_MODE_NONE, ERRMSG); + static_assert((int32_t)PerformanceMode::PowerSaving + == AAUDIO_PERFORMANCE_MODE_POWER_SAVING, ERRMSG); + static_assert((int32_t)PerformanceMode::LowLatency + == AAUDIO_PERFORMANCE_MODE_LOW_LATENCY, ERRMSG); + +// The aaudio_ usage, content and input_preset types were added in NDK 17, +// which is the first version to support Android Pie (API 28). +#if __NDK_MAJOR__ >= 17 + + ASSERT_INT32(aaudio_usage_t); + ASSERT_INT32(aaudio_content_type_t); + ASSERT_INT32(aaudio_input_preset_t); + + static_assert((int32_t)Usage::Media == AAUDIO_USAGE_MEDIA, ERRMSG); + static_assert((int32_t)Usage::VoiceCommunication == AAUDIO_USAGE_VOICE_COMMUNICATION, ERRMSG); + static_assert((int32_t)Usage::VoiceCommunicationSignalling + == AAUDIO_USAGE_VOICE_COMMUNICATION_SIGNALLING, ERRMSG); + static_assert((int32_t)Usage::Alarm == AAUDIO_USAGE_ALARM, ERRMSG); + static_assert((int32_t)Usage::Notification == AAUDIO_USAGE_NOTIFICATION, ERRMSG); + static_assert((int32_t)Usage::NotificationRingtone == AAUDIO_USAGE_NOTIFICATION_RINGTONE, ERRMSG); + static_assert((int32_t)Usage::NotificationEvent == AAUDIO_USAGE_NOTIFICATION_EVENT, ERRMSG); + static_assert((int32_t)Usage::AssistanceAccessibility == AAUDIO_USAGE_ASSISTANCE_ACCESSIBILITY, ERRMSG); + static_assert((int32_t)Usage::AssistanceNavigationGuidance + == AAUDIO_USAGE_ASSISTANCE_NAVIGATION_GUIDANCE, ERRMSG); + static_assert((int32_t)Usage::AssistanceSonification == AAUDIO_USAGE_ASSISTANCE_SONIFICATION, ERRMSG); + static_assert((int32_t)Usage::Game == AAUDIO_USAGE_GAME, ERRMSG); + static_assert((int32_t)Usage::Assistant == AAUDIO_USAGE_ASSISTANT, ERRMSG); + + static_assert((int32_t)ContentType::Speech == AAUDIO_CONTENT_TYPE_SPEECH, ERRMSG); + static_assert((int32_t)ContentType::Music == AAUDIO_CONTENT_TYPE_MUSIC, ERRMSG); + static_assert((int32_t)ContentType::Movie == AAUDIO_CONTENT_TYPE_MOVIE, ERRMSG); + static_assert((int32_t)ContentType::Sonification == AAUDIO_CONTENT_TYPE_SONIFICATION, ERRMSG); + + static_assert((int32_t)InputPreset::Generic == AAUDIO_INPUT_PRESET_GENERIC, ERRMSG); + static_assert((int32_t)InputPreset::Camcorder == AAUDIO_INPUT_PRESET_CAMCORDER, ERRMSG); + static_assert((int32_t)InputPreset::VoiceRecognition == AAUDIO_INPUT_PRESET_VOICE_RECOGNITION, ERRMSG); + static_assert((int32_t)InputPreset::VoiceCommunication + == AAUDIO_INPUT_PRESET_VOICE_COMMUNICATION, ERRMSG); + static_assert((int32_t)InputPreset::Unprocessed == AAUDIO_INPUT_PRESET_UNPROCESSED, ERRMSG); + + static_assert((int32_t)SessionId::None == AAUDIO_SESSION_ID_NONE, ERRMSG); + static_assert((int32_t)SessionId::Allocate == AAUDIO_SESSION_ID_ALLOCATE, ERRMSG); + +#endif // __NDK_MAJOR__ >= 17 + +// aaudio_allowed_capture_policy_t was added in NDK 20, +// which is the first version to support Android Q (API 29). +#if __NDK_MAJOR__ >= 20 + + ASSERT_INT32(aaudio_allowed_capture_policy_t); + + static_assert((int32_t)AllowedCapturePolicy::Unspecified == AAUDIO_UNSPECIFIED, ERRMSG); + static_assert((int32_t)AllowedCapturePolicy::All == AAUDIO_ALLOW_CAPTURE_BY_ALL, ERRMSG); + static_assert((int32_t)AllowedCapturePolicy::System == AAUDIO_ALLOW_CAPTURE_BY_SYSTEM, ERRMSG); + static_assert((int32_t)AllowedCapturePolicy::None == AAUDIO_ALLOW_CAPTURE_BY_NONE, ERRMSG); + +#endif // __NDK_MAJOR__ >= 20 + +// The aaudio channel masks and spatialization behavior were added in NDK 24, +// which is the first version to support Android SC_V2 (API 32). +#if __NDK_MAJOR__ >= 24 + + ASSERT_UINT32(aaudio_channel_mask_t); + + static_assert((uint32_t)ChannelMask::FrontLeft == AAUDIO_CHANNEL_FRONT_LEFT, ERRMSG); + static_assert((uint32_t)ChannelMask::FrontRight == AAUDIO_CHANNEL_FRONT_RIGHT, ERRMSG); + static_assert((uint32_t)ChannelMask::FrontCenter == AAUDIO_CHANNEL_FRONT_CENTER, ERRMSG); + static_assert((uint32_t)ChannelMask::LowFrequency == AAUDIO_CHANNEL_LOW_FREQUENCY, ERRMSG); + static_assert((uint32_t)ChannelMask::BackLeft == AAUDIO_CHANNEL_BACK_LEFT, ERRMSG); + static_assert((uint32_t)ChannelMask::BackRight == AAUDIO_CHANNEL_BACK_RIGHT, ERRMSG); + static_assert((uint32_t)ChannelMask::FrontLeftOfCenter == AAUDIO_CHANNEL_FRONT_LEFT_OF_CENTER, ERRMSG); + static_assert((uint32_t)ChannelMask::FrontRightOfCenter == AAUDIO_CHANNEL_FRONT_RIGHT_OF_CENTER, ERRMSG); + static_assert((uint32_t)ChannelMask::BackCenter == AAUDIO_CHANNEL_BACK_CENTER, ERRMSG); + static_assert((uint32_t)ChannelMask::SideLeft == AAUDIO_CHANNEL_SIDE_LEFT, ERRMSG); + static_assert((uint32_t)ChannelMask::SideRight == AAUDIO_CHANNEL_SIDE_RIGHT, ERRMSG); + static_assert((uint32_t)ChannelMask::TopCenter == AAUDIO_CHANNEL_TOP_CENTER, ERRMSG); + static_assert((uint32_t)ChannelMask::TopFrontLeft == AAUDIO_CHANNEL_TOP_FRONT_LEFT, ERRMSG); + static_assert((uint32_t)ChannelMask::TopFrontCenter == AAUDIO_CHANNEL_TOP_FRONT_CENTER, ERRMSG); + static_assert((uint32_t)ChannelMask::TopFrontRight == AAUDIO_CHANNEL_TOP_FRONT_RIGHT, ERRMSG); + static_assert((uint32_t)ChannelMask::TopBackLeft == AAUDIO_CHANNEL_TOP_BACK_LEFT, ERRMSG); + static_assert((uint32_t)ChannelMask::TopBackCenter == AAUDIO_CHANNEL_TOP_BACK_CENTER, ERRMSG); + static_assert((uint32_t)ChannelMask::TopBackRight == AAUDIO_CHANNEL_TOP_BACK_RIGHT, ERRMSG); + static_assert((uint32_t)ChannelMask::TopSideLeft == AAUDIO_CHANNEL_TOP_SIDE_LEFT, ERRMSG); + static_assert((uint32_t)ChannelMask::TopSideRight == AAUDIO_CHANNEL_TOP_SIDE_RIGHT, ERRMSG); + static_assert((uint32_t)ChannelMask::BottomFrontLeft == AAUDIO_CHANNEL_BOTTOM_FRONT_LEFT, ERRMSG); + static_assert((uint32_t)ChannelMask::BottomFrontCenter == AAUDIO_CHANNEL_BOTTOM_FRONT_CENTER, ERRMSG); + static_assert((uint32_t)ChannelMask::BottomFrontRight == AAUDIO_CHANNEL_BOTTOM_FRONT_RIGHT, ERRMSG); + static_assert((uint32_t)ChannelMask::LowFrequency2 == AAUDIO_CHANNEL_LOW_FREQUENCY_2, ERRMSG); + static_assert((uint32_t)ChannelMask::FrontWideLeft == AAUDIO_CHANNEL_FRONT_WIDE_LEFT, ERRMSG); + static_assert((uint32_t)ChannelMask::FrontWideRight == AAUDIO_CHANNEL_FRONT_WIDE_RIGHT, ERRMSG); + static_assert((uint32_t)ChannelMask::Mono == AAUDIO_CHANNEL_MONO, ERRMSG); + static_assert((uint32_t)ChannelMask::Stereo == AAUDIO_CHANNEL_STEREO, ERRMSG); + static_assert((uint32_t)ChannelMask::CM2Point1 == AAUDIO_CHANNEL_2POINT1, ERRMSG); + static_assert((uint32_t)ChannelMask::Tri == AAUDIO_CHANNEL_TRI, ERRMSG); + static_assert((uint32_t)ChannelMask::TriBack == AAUDIO_CHANNEL_TRI_BACK, ERRMSG); + static_assert((uint32_t)ChannelMask::CM3Point1 == AAUDIO_CHANNEL_3POINT1, ERRMSG); + static_assert((uint32_t)ChannelMask::CM2Point0Point2 == AAUDIO_CHANNEL_2POINT0POINT2, ERRMSG); + static_assert((uint32_t)ChannelMask::CM2Point1Point2 == AAUDIO_CHANNEL_2POINT1POINT2, ERRMSG); + static_assert((uint32_t)ChannelMask::CM3Point0Point2 == AAUDIO_CHANNEL_3POINT0POINT2, ERRMSG); + static_assert((uint32_t)ChannelMask::CM3Point1Point2 == AAUDIO_CHANNEL_3POINT1POINT2, ERRMSG); + static_assert((uint32_t)ChannelMask::Quad == AAUDIO_CHANNEL_QUAD, ERRMSG); + static_assert((uint32_t)ChannelMask::QuadSide == AAUDIO_CHANNEL_QUAD_SIDE, ERRMSG); + static_assert((uint32_t)ChannelMask::Surround == AAUDIO_CHANNEL_SURROUND, ERRMSG); + static_assert((uint32_t)ChannelMask::Penta == AAUDIO_CHANNEL_PENTA, ERRMSG); + static_assert((uint32_t)ChannelMask::CM5Point1 == AAUDIO_CHANNEL_5POINT1, ERRMSG); + static_assert((uint32_t)ChannelMask::CM5Point1Side == AAUDIO_CHANNEL_5POINT1_SIDE, ERRMSG); + static_assert((uint32_t)ChannelMask::CM6Point1 == AAUDIO_CHANNEL_6POINT1, ERRMSG); + static_assert((uint32_t)ChannelMask::CM7Point1 == AAUDIO_CHANNEL_7POINT1, ERRMSG); + static_assert((uint32_t)ChannelMask::CM5Point1Point2 == AAUDIO_CHANNEL_5POINT1POINT2, ERRMSG); + static_assert((uint32_t)ChannelMask::CM5Point1Point4 == AAUDIO_CHANNEL_5POINT1POINT4, ERRMSG); + static_assert((uint32_t)ChannelMask::CM7Point1Point2 == AAUDIO_CHANNEL_7POINT1POINT2, ERRMSG); + static_assert((uint32_t)ChannelMask::CM7Point1Point4 == AAUDIO_CHANNEL_7POINT1POINT4, ERRMSG); + static_assert((uint32_t)ChannelMask::CM9Point1Point4 == AAUDIO_CHANNEL_9POINT1POINT4, ERRMSG); + static_assert((uint32_t)ChannelMask::CM9Point1Point6 == AAUDIO_CHANNEL_9POINT1POINT6, ERRMSG); + static_assert((uint32_t)ChannelMask::FrontBack == AAUDIO_CHANNEL_FRONT_BACK, ERRMSG); + + ASSERT_INT32(aaudio_spatialization_behavior_t); + + static_assert((int32_t)SpatializationBehavior::Unspecified == AAUDIO_UNSPECIFIED, ERRMSG); + static_assert((int32_t)SpatializationBehavior::Auto == AAUDIO_SPATIALIZATION_BEHAVIOR_AUTO, ERRMSG); + static_assert((int32_t)SpatializationBehavior::Never == AAUDIO_SPATIALIZATION_BEHAVIOR_NEVER, ERRMSG); + +#endif + +// The aaudio device type and aaudio policy were added in NDK 29, +// which is the first version to support Android B (API 36). +#if __NDK_MAJOR__ >= 30 + + ASSERT_INT32(AAudio_DeviceType); + static_assert((int32_t)DeviceType::BuiltinEarpiece == AAUDIO_DEVICE_BUILTIN_EARPIECE, ERRMSG); + static_assert((int32_t)DeviceType::BuiltinSpeaker == AAUDIO_DEVICE_BUILTIN_SPEAKER, ERRMSG); + static_assert((int32_t)DeviceType::WiredHeadset == AAUDIO_DEVICE_WIRED_HEADSET, ERRMSG); + static_assert((int32_t)DeviceType::WiredHeadphones == AAUDIO_DEVICE_WIRED_HEADPHONES, ERRMSG); + static_assert((int32_t)DeviceType::LineAnalog == AAUDIO_DEVICE_LINE_ANALOG, ERRMSG); + static_assert((int32_t)DeviceType::LineDigital == AAUDIO_DEVICE_LINE_DIGITAL, ERRMSG); + static_assert((int32_t)DeviceType::BluetoothSco == AAUDIO_DEVICE_BLUETOOTH_SCO, ERRMSG); + static_assert((int32_t)DeviceType::BluetoothA2dp == AAUDIO_DEVICE_BLUETOOTH_A2DP, ERRMSG); + static_assert((int32_t)DeviceType::Hdmi == AAUDIO_DEVICE_HDMI, ERRMSG); + static_assert((int32_t)DeviceType::HdmiArc == AAUDIO_DEVICE_HDMI_ARC, ERRMSG); + static_assert((int32_t)DeviceType::UsbDevice == AAUDIO_DEVICE_USB_DEVICE, ERRMSG); + static_assert((int32_t)DeviceType::UsbAccessory == AAUDIO_DEVICE_USB_ACCESSORY, ERRMSG); + static_assert((int32_t)DeviceType::Dock == AAUDIO_DEVICE_DOCK, ERRMSG); + static_assert((int32_t)DeviceType::FM == AAUDIO_DEVICE_FM, ERRMSG); + static_assert((int32_t)DeviceType::BuiltinMic == AAUDIO_DEVICE_BUILTIN_MIC, ERRMSG); + static_assert((int32_t)DeviceType::FMTuner == AAUDIO_DEVICE_FM_TUNER, ERRMSG); + static_assert((int32_t)DeviceType::TVTuner == AAUDIO_DEVICE_TV_TUNER, ERRMSG); + static_assert((int32_t)DeviceType::Telephony == AAUDIO_DEVICE_TELEPHONY, ERRMSG); + static_assert((int32_t)DeviceType::AuxLine == AAUDIO_DEVICE_AUX_LINE, ERRMSG); + static_assert((int32_t)DeviceType::IP == AAUDIO_DEVICE_IP, ERRMSG); + static_assert((int32_t)DeviceType::Bus == AAUDIO_DEVICE_BUS, ERRMSG); + static_assert((int32_t)DeviceType::UsbHeadset == AAUDIO_DEVICE_USB_HEADSET, ERRMSG); + static_assert((int32_t)DeviceType::HearingAid == AAUDIO_DEVICE_HEARING_AID, ERRMSG); + static_assert((int32_t)DeviceType::BuiltinSpeakerSafe == AAUDIO_DEVICE_BUILTIN_SPEAKER_SAFE, ERRMSG); + static_assert((int32_t)DeviceType::RemoteSubmix == AAUDIO_DEVICE_REMOTE_SUBMIX, ERRMSG); + static_assert((int32_t)DeviceType::BleHeadset == AAUDIO_DEVICE_BLE_HEADSET, ERRMSG); + static_assert((int32_t)DeviceType::BleSpeaker == AAUDIO_DEVICE_BLE_SPEAKER, ERRMSG); + static_assert((int32_t)DeviceType::HdmiEarc == AAUDIO_DEVICE_HDMI_EARC, ERRMSG); + static_assert((int32_t)DeviceType::BleBroadcast == AAUDIO_DEVICE_BLE_BROADCAST, ERRMSG); + static_assert((int32_t)DeviceType::DockAnalog == AAUDIO_DEVICE_DOCK_ANALOG, ERRMSG); + + ASSERT_INT32(aaudio_policy_t); + static_assert((int32_t)MMapPolicy::Unspecified == AAUDIO_UNSPECIFIED, ERRMSG); + static_assert((int32_t)MMapPolicy::Never == AAUDIO_POLICY_NEVER, ERRMSG); + static_assert((int32_t)MMapPolicy::Auto == AAUDIO_POLICY_AUTO, ERRMSG); + static_assert((int32_t)MMapPolicy::Always == AAUDIO_POLICY_ALWAYS, ERRMSG); + +#endif // __NDK_MAJOR__ >= 29 + +#endif // AAUDIO_AAUDIO_H + +} // namespace oboe diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_aaudio_AAudioLoader_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_aaudio_AAudioLoader_android.h new file mode 100644 index 0000000..523021a --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_aaudio_AAudioLoader_android.h @@ -0,0 +1,396 @@ +/* + * Copyright 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_AAUDIO_LOADER_H_ +#define OBOE_AAUDIO_LOADER_H_ + +#include +#include "oboe_oboe_Definitions_android.h" + +// If the NDK is before O then define this in your build +// so that AAudio.h will not be included. +#ifdef OBOE_NO_INCLUDE_AAUDIO + +// Define missing types from AAudio.h +typedef int32_t aaudio_stream_state_t; +typedef int32_t aaudio_direction_t; +typedef int32_t aaudio_format_t; +typedef int32_t aaudio_data_callback_result_t; +typedef int32_t aaudio_result_t; +typedef int32_t aaudio_sharing_mode_t; +typedef int32_t aaudio_performance_mode_t; + +typedef struct AAudioStreamStruct AAudioStream; +typedef struct AAudioStreamBuilderStruct AAudioStreamBuilder; + +typedef aaudio_data_callback_result_t (*AAudioStream_dataCallback)( + AAudioStream *stream, + void *userData, + void *audioData, + int32_t numFrames); + +typedef void (*AAudioStream_errorCallback)( + AAudioStream *stream, + void *userData, + aaudio_result_t error); + +// These were defined in P +typedef int32_t aaudio_usage_t; +typedef int32_t aaudio_content_type_t; +typedef int32_t aaudio_input_preset_t; +typedef int32_t aaudio_session_id_t; + +// There are a few definitions used by Oboe. +#define AAUDIO_OK static_cast(Result::OK) +#define AAUDIO_ERROR_TIMEOUT static_cast(Result::ErrorTimeout) +#define AAUDIO_STREAM_STATE_STARTING static_cast(StreamState::Starting) +#define AAUDIO_STREAM_STATE_STARTED static_cast(StreamState::Started) +#else +#include +#endif + +#ifdef __NDK_MAJOR__ +#define OBOE_USING_NDK 1 +#else +#define __NDK_MAJOR__ 0 +#define OBOE_USING_NDK 0 +#endif + +#if __NDK_MAJOR__ < 24 +// Defined in SC_V2 +typedef uint32_t aaudio_channel_mask_t; +typedef int32_t aaudio_spatialization_behavior_t; +#endif + +#if OBOE_USING_NDK && __NDK_MAJOR__ < 29 +// Defined in Android B +typedef void (*AAudioStream_presentationEndCallback)( + AAudioStream* stream, + void* userData); +#endif + +#ifndef __ANDROID_API_Q__ +#define __ANDROID_API_Q__ 29 +#endif + +#ifndef __ANDROID_API_R__ +#define __ANDROID_API_R__ 30 +#endif + +#ifndef __ANDROID_API_S__ +#define __ANDROID_API_S__ 31 +#endif + +#ifndef __ANDROID_API_S_V2__ +#define __ANDROID_API_S_V2__ 32 +#endif + +#ifndef __ANDROID_API_U__ +#define __ANDROID_API_U__ 34 +#endif + +#ifndef __ANDROID_API_B__ +#define __ANDROID_API_B__ 36 +#endif + +#if OBOE_USING_NDK && __NDK_MAJOR__ < 30 +// These were defined in Android B +typedef int32_t AAudio_DeviceType; +typedef int32_t aaudio_policy_t; +#endif + +// TODO: find the first NDK version containing the following values +#if OBOE_USING_NDK && __NDK_MAJOR__ <= 30 +typedef enum AAudio_FallbackMode : int32_t { + AAUDIO_FALLBACK_MODE_DEFAULT = 0, + AAUDIO_FALLBACK_MODE_MUTE = 1, + AAUDIO_FALLBACK_MODE_FAIL = 2, +} AAudio_FallbackMode; + +typedef enum AAudio_StretchMode : int32_t { + AAUDIO_STRETCH_MODE_DEFAULT = 0, + AAUDIO_STRETCH_MODE_VOICE = 1, +} AAudio_StretchMode; + +typedef struct AAudioPlaybackParameters { + AAudio_FallbackMode fallbackMode; + AAudio_StretchMode stretchMode; + float pitch; + float speed; +} AAudioPlaybackParameters; + +typedef int32_t (*AAudioStream_partialDataCallback)( + AAudioStream* stream, + void* userData, + void* audioData, + int32_t numFrames); +#endif + +namespace oboe { + +/** + * The AAudio API was not available in early versions of Android. + * To avoid linker errors, we dynamically link with the functions by name using dlsym(). + * On older versions this linkage will safely fail. + */ +class AAudioLoader { + public: + // Use signatures for common functions. + // Key to letter abbreviations. + // S = Stream + // B = Builder + // I = int32_t + // L = int64_t + // T = sTate + // K = clocKid_t + // P = Pointer to following data type + // C = Const prefix + // H = cHar + // U = uint32_t + // O = bOol + // R = pResentation end callback + // M = aaudioplaybackparaMeters + // D = Datacallback/partialDatacallback + + typedef int32_t (*signature_I_PPB)(AAudioStreamBuilder **builder); + + typedef const char * (*signature_CPH_I)(int32_t); + + typedef int32_t (*signature_I_PBPPS)(AAudioStreamBuilder *, + AAudioStream **stream); // AAudioStreamBuilder_open() + + typedef int32_t (*signature_I_PB)(AAudioStreamBuilder *); // AAudioStreamBuilder_delete() + // AAudioStreamBuilder_setSampleRate() + typedef void (*signature_V_PBI)(AAudioStreamBuilder *, int32_t); + + // AAudioStreamBuilder_setChannelMask() + typedef void (*signature_V_PBU)(AAudioStreamBuilder *, uint32_t); + + typedef void (*signature_V_PBCPH)(AAudioStreamBuilder *, const char *); + + // AAudioStreamBuilder_setPrivacySensitive + typedef void (*signature_V_PBO)(AAudioStreamBuilder *, bool); + + typedef int32_t (*signature_I_PS)(AAudioStream *); // AAudioStream_getSampleRate() + typedef int64_t (*signature_L_PS)(AAudioStream *); // AAudioStream_getFramesRead() + // AAudioStream_setBufferSizeInFrames() + typedef int32_t (*signature_I_PSI)(AAudioStream *, int32_t); + + typedef void (*signature_V_PBPDPV)(AAudioStreamBuilder *, + AAudioStream_dataCallback, + void *); + + typedef void (*signature_V_PBPEPV)(AAudioStreamBuilder *, + AAudioStream_errorCallback, + void *); + + typedef void (*signature_V_PBPRPV)(AAudioStreamBuilder *, + AAudioStream_presentationEndCallback, + void *); + + typedef aaudio_format_t (*signature_F_PS)(AAudioStream *stream); + + typedef int32_t (*signature_I_PSPVIL)(AAudioStream *, void *, int32_t, int64_t); + typedef int32_t (*signature_I_PSCPVIL)(AAudioStream *, const void *, int32_t, int64_t); + + typedef int32_t (*signature_I_PSTPTL)(AAudioStream *, + aaudio_stream_state_t, + aaudio_stream_state_t *, + int64_t); + + typedef int32_t (*signature_I_PSKPLPL)(AAudioStream *, clockid_t, int64_t *, int64_t *); + + typedef bool (*signature_O_PS)(AAudioStream *); + + typedef uint32_t (*signature_U_PS)(AAudioStream *); + + typedef int32_t (*signature_I_II)(int32_t, int32_t); + typedef int32_t (*signature_I_I)(int32_t); + typedef int32_t (*signature_I)(); + typedef int32_t (*signature_I_PSII)(AAudioStream *, int32_t, int32_t); + + // AAudioStream_getDeviceIds() + typedef int32_t (*signature_I_PSPIPI)(AAudioStream *, int32_t *, int32_t *); + + typedef int32_t (*signature_I_PSIPL)(AAudioStream *, int32_t, int64_t *); + + typedef int32_t (*signature_I_PSPM)(AAudioStream *, AAudioPlaybackParameters *); + typedef int32_t (*signature_I_PSCPM)(AAudioStream *, const AAudioPlaybackParameters *); + + static AAudioLoader* getInstance(); // singleton + + /** + * Open the AAudio shared library and load the function pointers. + * This can be called multiple times. + * It should only be called from one thread. + * + * The destructor will clean up after the open. + * + * @return 0 if successful or negative error. + */ + int open(); + + void *getLibHandle() const { return mLibHandle; } + + // Function pointers into the AAudio shared library. + signature_I_PPB createStreamBuilder = nullptr; + + signature_I_PBPPS builder_openStream = nullptr; + + signature_V_PBI builder_setBufferCapacityInFrames = nullptr; + signature_V_PBI builder_setChannelCount = nullptr; + signature_V_PBI builder_setDeviceId = nullptr; + signature_V_PBI builder_setDirection = nullptr; + signature_V_PBI builder_setFormat = nullptr; + signature_V_PBI builder_setFramesPerDataCallback = nullptr; + signature_V_PBI builder_setPerformanceMode = nullptr; + signature_V_PBI builder_setSampleRate = nullptr; + signature_V_PBI builder_setSharingMode = nullptr; + signature_V_PBU builder_setChannelMask = nullptr; + + signature_V_PBI builder_setUsage = nullptr; + signature_V_PBI builder_setContentType = nullptr; + signature_V_PBI builder_setInputPreset = nullptr; + signature_V_PBI builder_setSessionId = nullptr; + + signature_V_PBO builder_setPrivacySensitive = nullptr; + signature_V_PBI builder_setAllowedCapturePolicy = nullptr; + + signature_V_PBCPH builder_setPackageName = nullptr; + signature_V_PBCPH builder_setAttributionTag = nullptr; + + signature_V_PBO builder_setIsContentSpatialized = nullptr; + signature_V_PBI builder_setSpatializationBehavior = nullptr; + + signature_V_PBPDPV builder_setDataCallback = nullptr; + signature_V_PBPEPV builder_setErrorCallback = nullptr; + signature_V_PBPRPV builder_setPresentationEndCallback = nullptr; + signature_V_PBPDPV builder_setPartialDataCallback = nullptr; + + signature_I_PB builder_delete = nullptr; + + signature_F_PS stream_getFormat = nullptr; + + signature_I_PSPVIL stream_read = nullptr; + signature_I_PSCPVIL stream_write = nullptr; + + signature_I_PSTPTL stream_waitForStateChange = nullptr; + + signature_I_PSKPLPL stream_getTimestamp = nullptr; + + signature_I_PSPIPI stream_getDeviceIds = nullptr; + + signature_I_PS stream_release = nullptr; + signature_I_PS stream_close = nullptr; + + signature_I_PS stream_getChannelCount = nullptr; + signature_I_PS stream_getDeviceId = nullptr; + + signature_I_PS stream_getBufferSize = nullptr; + signature_I_PS stream_getBufferCapacity = nullptr; + signature_I_PS stream_getFramesPerBurst = nullptr; + signature_I_PS stream_getState = nullptr; + signature_I_PS stream_getPerformanceMode = nullptr; + signature_I_PS stream_getSampleRate = nullptr; + signature_I_PS stream_getSharingMode = nullptr; + signature_I_PS stream_getXRunCount = nullptr; + + signature_I_PSI stream_setBufferSize = nullptr; + signature_I_PS stream_requestStart = nullptr; + signature_I_PS stream_requestPause = nullptr; + signature_I_PS stream_requestFlush = nullptr; + signature_I_PS stream_requestStop = nullptr; + + signature_L_PS stream_getFramesRead = nullptr; + signature_L_PS stream_getFramesWritten = nullptr; + + signature_CPH_I convertResultToText = nullptr; + + signature_I_PS stream_getUsage = nullptr; + signature_I_PS stream_getContentType = nullptr; + signature_I_PS stream_getInputPreset = nullptr; + signature_I_PS stream_getSessionId = nullptr; + + signature_O_PS stream_isPrivacySensitive = nullptr; + signature_I_PS stream_getAllowedCapturePolicy = nullptr; + + signature_U_PS stream_getChannelMask = nullptr; + + signature_O_PS stream_isContentSpatialized = nullptr; + signature_I_PS stream_getSpatializationBehavior = nullptr; + + signature_I_PS stream_getHardwareChannelCount = nullptr; + signature_I_PS stream_getHardwareSampleRate = nullptr; + signature_F_PS stream_getHardwareFormat = nullptr; + + + signature_I_II aaudio_getPlatformMMapPolicy = nullptr; + signature_I_II aaudio_getPlatformMMapExclusivePolicy = nullptr; + signature_I_I aaudio_setMMapPolicy = nullptr; + signature_I aaudio_getMMapPolicy = nullptr; + signature_O_PS stream_isMMapUsed = nullptr; + + signature_I_PSII stream_setOffloadDelayPadding = nullptr; + signature_I_PS stream_getOffloadDelay = nullptr; + signature_I_PS stream_getOffloadPadding = nullptr; + signature_I_PS stream_setOffloadEndOfStream = nullptr; + + signature_I_PSIPL stream_flushFromFrame = nullptr; + + signature_I_PSPM stream_getPlaybackParameters = nullptr; + signature_I_PSCPM stream_setPlaybackParameters = nullptr; + + private: + AAudioLoader() {} + ~AAudioLoader(); + + // Load function pointers for specific signatures. + signature_I_PPB load_I_PPB(const char *name); + signature_CPH_I load_CPH_I(const char *name); + signature_V_PBI load_V_PBI(const char *name); + signature_V_PBCPH load_V_PBCPH(const char *name); + signature_V_PBPDPV load_V_PBPDPV(const char *name); + signature_V_PBPEPV load_V_PBPEPV(const char *name); + signature_I_PB load_I_PB(const char *name); + signature_I_PBPPS load_I_PBPPS(const char *name); + signature_I_PS load_I_PS(const char *name); + signature_L_PS load_L_PS(const char *name); + signature_F_PS load_F_PS(const char *name); + signature_O_PS load_O_PS(const char *name); + signature_I_PSI load_I_PSI(const char *name); + signature_I_PSPVIL load_I_PSPVIL(const char *name); + signature_I_PSCPVIL load_I_PSCPVIL(const char *name); + signature_I_PSTPTL load_I_PSTPTL(const char *name); + signature_I_PSKPLPL load_I_PSKPLPL(const char *name); + signature_V_PBU load_V_PBU(const char *name); + signature_U_PS load_U_PS(const char *name); + signature_V_PBO load_V_PBO(const char *name); + signature_I_II load_I_II(const char *name); + signature_I_I load_I_I(const char *name); + signature_I load_I(const char *name); + signature_V_PBPRPV load_V_PBPRPV(const char *name); + signature_I_PSII load_I_PSII(const char *name); + signature_I_PSPIPI load_I_PSPIPI(const char *name); + signature_I_PSIPL load_I_PSIPL(const char *name); + signature_I_PSPM load_I_PSPM(const char *name); + signature_I_PSCPM load_I_PSCPM(const char *name); + + void *mLibHandle = nullptr; +}; + +} // namespace oboe + +#endif //OBOE_AAUDIO_LOADER_H_ diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_aaudio_AudioStreamAAudio_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_aaudio_AudioStreamAAudio_android.cpp new file mode 100644 index 0000000..ba43255 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_aaudio_AudioStreamAAudio_android.cpp @@ -0,0 +1,1186 @@ +/* + * Copyright 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include "oboe_aaudio_AAudioLoader_android.h" +#include "oboe_aaudio_AudioStreamAAudio_android.h" +#include "oboe_common_OboeDebug_android.h" +#include "oboe_oboe_AudioClock_android.h" +#include "oboe_oboe_Utilities_android.h" +#include "oboe_aaudio_AAudioExtensions_android.h" + +#ifdef __ANDROID__ +#include +#include "oboe_common_QuirksManager_android.h" + +#endif + +#ifndef OBOE_FIX_FORCE_STARTING_TO_STARTED +// Workaround state problems in AAudio +// TODO Which versions does this occur in? Verify fixed in Q. +#define OBOE_FIX_FORCE_STARTING_TO_STARTED 1 +#endif // OBOE_FIX_FORCE_STARTING_TO_STARTED + +using namespace oboe; +AAudioLoader *AudioStreamAAudio::mLibLoader = nullptr; + +// 'C' wrapper for the data callback method +static aaudio_data_callback_result_t oboe_aaudio_data_callback_proc( + AAudioStream *stream, + void *userData, + void *audioData, + int32_t numFrames) { + + AudioStreamAAudio *oboeStream = reinterpret_cast(userData); + if (oboeStream != nullptr) { + return static_cast( + oboeStream->callOnAudioReady(stream, audioData, numFrames)); + + } else { + return static_cast(DataCallbackResult::Stop); + } +} + +// 'C' wrapper for the partial data callback method +static int32_t oboe_aaudio_partial_data_callback_proc( + AAudioStream *stream, + void *userData, + void *audioData, + int32_t numFrames) { + AudioStreamAAudio *oboeStream = reinterpret_cast(userData); + if (oboeStream != nullptr) { + return oboeStream->callOnPartialAudioReady(stream, audioData, numFrames); + } else { + // Return negative number to stop the stream. + return -1; + } +} + +// This runs in its own thread. +// Only one of these threads will be launched from internalErrorCallback(). +// It calls app error callbacks from a static function in case the stream gets deleted. +static void oboe_aaudio_error_thread_proc_common(AudioStreamAAudio *oboeStream, + Result error) { +#if 0 + LOGE("%s() sleep for 5 seconds", __func__); + usleep(5*1000*1000); + LOGD("%s() - woke up -------------------------", __func__); +#endif + AudioStreamErrorCallback *errorCallback = oboeStream->getErrorCallback(); + if (errorCallback == nullptr) return; // should be impossible + bool isErrorHandled = errorCallback->onError(oboeStream, error); + + if (!isErrorHandled) { + oboeStream->requestStop(); + errorCallback->onErrorBeforeClose(oboeStream, error); + oboeStream->close(); + // Warning, oboeStream may get deleted by this callback. + errorCallback->onErrorAfterClose(oboeStream, error); + } +} + +// Callback thread for raw pointers. +static void oboe_aaudio_error_thread_proc(AudioStreamAAudio *oboeStream, + Result error) { + LOGD("%s(,%d) - entering >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>", __func__, error); + oboe_aaudio_error_thread_proc_common(oboeStream, error); + LOGD("%s() - exiting <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<", __func__); +} + +// Callback thread for shared pointers. +static void oboe_aaudio_error_thread_proc_shared(std::shared_ptr sharedStream, + Result error) { + LOGD("%s(,%d) - entering >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>", __func__, error); + // Hold the shared pointer while we use the raw pointer. + AudioStreamAAudio *oboeStream = reinterpret_cast(sharedStream.get()); + oboe_aaudio_error_thread_proc_common(oboeStream, error); + LOGD("%s() - exiting <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<", __func__); +} + +static void oboe_aaudio_presentation_thread_proc_common(AudioStreamAAudio *oboeStream) { + auto presentationCallback = oboeStream->getPresentationCallback(); + if (presentationCallback == nullptr) return; // should be impossible + presentationCallback->onPresentationEnded(oboeStream); +} + +// Callback thread for raw pointers +static void oboe_aaudio_presentation_thread_proc(AudioStreamAAudio *oboeStream) { + LOGD("%s() - entering >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>", __func__); + oboe_aaudio_presentation_thread_proc_common(oboeStream); + LOGD("%s() - exiting <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<", __func__); +} + +// Callback thread for shared pointers +static void oboe_aaudio_presentation_end_thread_proc_shared( + std::shared_ptr sharedStream) { + LOGD("%s() - entering >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>", __func__); + AudioStreamAAudio *oboeStream = reinterpret_cast(sharedStream.get()); + oboe_aaudio_presentation_thread_proc_common(oboeStream); + LOGD("%s() - exiting <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<", __func__); +} + +namespace oboe { + +/* + * Create a stream that uses Oboe Audio API. + */ +AudioStreamAAudio::AudioStreamAAudio(const AudioStreamBuilder &builder) + : AudioStream(builder) + , mAAudioStream(nullptr) { + mCallbackThreadEnabled.store(false); + mLibLoader = AAudioLoader::getInstance(); +} + +bool AudioStreamAAudio::isSupported() { + mLibLoader = AAudioLoader::getInstance(); + int openResult = mLibLoader->open(); + return openResult == 0; +} + +// Static method for the error callback. +// We use a method so we can access protected methods on the stream. +// Launch a thread to handle the error. +// That other thread can safely stop, close and delete the stream. +void AudioStreamAAudio::internalErrorCallback( + AAudioStream *stream, + void *userData, + aaudio_result_t error) { + oboe::Result oboeResult = static_cast(error); + AudioStreamAAudio *oboeStream = reinterpret_cast(userData); + + // Coerce the error code if needed to workaround a regression in RQ1A that caused + // the wrong code to be passed when headsets plugged in. See b/173928197. + if (OboeGlobals::areWorkaroundsEnabled() + && getSdkVersion() == __ANDROID_API_R__ + && oboeResult == oboe::Result::ErrorTimeout) { + oboeResult = oboe::Result::ErrorDisconnected; + LOGD("%s() ErrorTimeout changed to ErrorDisconnected to fix b/173928197", __func__); + } + + oboeStream->mErrorCallbackResult = oboeResult; + + // Prevents deletion of the stream if the app is using AudioStreamBuilder::openStream(shared_ptr) + std::shared_ptr sharedStream = oboeStream->lockWeakThis(); + + // These checks should be enough because we assume that the stream close() + // will join() any active callback threads and will not allow new callbacks. + if (oboeStream->wasErrorCallbackCalled()) { // block extra error callbacks + LOGE("%s() multiple error callbacks called!", __func__); + } else if (stream != oboeStream->getUnderlyingStream()) { + LOGW("%s() stream already closed or closing", __func__); // might happen if there are bugs + } else if (sharedStream) { + // Handle error on a separate thread using shared pointer. + std::thread t(oboe_aaudio_error_thread_proc_shared, sharedStream, oboeResult); + t.detach(); + } else { + // Handle error on a separate thread. + std::thread t(oboe_aaudio_error_thread_proc, oboeStream, oboeResult); + t.detach(); + } +} + +void AudioStreamAAudio::beginPerformanceHintInCallback() { + if (isPerformanceHintEnabled()) { + if (!mAdpfOpenAttempted) { + int64_t targetDurationNanos = (mFramesPerBurst * 1e9) / getSampleRate(); + // This has to be called from the callback thread so we get the right TID. + int adpfResult = mAdpfWrapper.open(gettid(), targetDurationNanos); + if (adpfResult < 0) { + LOGW("WARNING ADPF not supported, %d\n", adpfResult); + } else { + LOGD("ADPF is now active\n"); + } + mAdpfOpenAttempted = true; + } + mAdpfWrapper.onBeginCallback(); + } else if (!isPerformanceHintEnabled() && mAdpfOpenAttempted) { + LOGD("ADPF closed\n"); + mAdpfWrapper.close(); + mAdpfOpenAttempted = false; + } +} + +void AudioStreamAAudio::endPerformanceHintInCallback(int32_t numFrames) { + if (mAdpfWrapper.isOpen()) { + // Scale the measured duration based on numFrames so it is normalized to a full burst. + double durationScaler = static_cast(mFramesPerBurst) / numFrames; + // Skip this callback if numFrames is very small. + // This can happen when buffers wrap around, particularly when doing sample rate conversion. + if (durationScaler < 2.0) { + mAdpfWrapper.onEndCallback(durationScaler); + } + } +} + +void AudioStreamAAudio::logUnsupportedAttributes() { + int sdkVersion = getSdkVersion(); + + // These attributes are not supported pre Android "P" + if (sdkVersion < __ANDROID_API_P__) { + if (mUsage != Usage::Media) { + LOGW("Usage [AudioStreamBuilder::setUsage()] " + "is not supported on AAudio streams running on pre-Android P versions."); + } + + if (mContentType != ContentType::Music) { + LOGW("ContentType [AudioStreamBuilder::setContentType()] " + "is not supported on AAudio streams running on pre-Android P versions."); + } + + if (mSessionId != SessionId::None) { + LOGW("SessionId [AudioStreamBuilder::setSessionId()] " + "is not supported on AAudio streams running on pre-Android P versions."); + } + } +} + +Result AudioStreamAAudio::open() { + Result result = Result::OK; + + if (mAAudioStream != nullptr) { + return Result::ErrorInvalidState; + } + + result = AudioStream::open(); + if (result != Result::OK) { + return result; + } + + AAudioStreamBuilder *aaudioBuilder; + result = static_cast(mLibLoader->createStreamBuilder(&aaudioBuilder)); + if (result != Result::OK) { + return result; + } + + // Do not set INPUT capacity below 4096 because that prevents us from getting a FAST track + // when using the Legacy data path. + // If the app requests > 4096 then we allow it but we are less likely to get LowLatency. + // See internal bug b/80308183 for more details. + // Fixed in Q but let's still clip the capacity because high input capacity + // does not increase latency. + int32_t capacity = mBufferCapacityInFrames; + constexpr int kCapacityRequiredForFastLegacyTrack = 4096; // matches value in AudioFinger + if (OboeGlobals::areWorkaroundsEnabled() + && mDirection == oboe::Direction::Input + && capacity != oboe::Unspecified + && capacity < kCapacityRequiredForFastLegacyTrack + && mPerformanceMode == oboe::PerformanceMode::LowLatency) { + capacity = kCapacityRequiredForFastLegacyTrack; + LOGD("AudioStreamAAudio.open() capacity changed from %d to %d for lower latency", + static_cast(mBufferCapacityInFrames), capacity); + } + mLibLoader->builder_setBufferCapacityInFrames(aaudioBuilder, capacity); + + if (mLibLoader->builder_setSessionId != nullptr) { + mLibLoader->builder_setSessionId(aaudioBuilder, + static_cast(mSessionId)); + // Output effects do not support PerformanceMode::LowLatency. + if (OboeGlobals::areWorkaroundsEnabled() + && mSessionId != SessionId::None + && mDirection == oboe::Direction::Output + && mPerformanceMode == PerformanceMode::LowLatency) { + mPerformanceMode = PerformanceMode::None; + LOGD("AudioStreamAAudio.open() performance mode changed to None when session " + "id is requested"); + } + } + + // Channel mask was added in SC_V2. Given the corresponding channel count of selected channel + // mask may be different from selected channel count, the last set value will be respected. + // If channel count is set after channel mask, the previously set channel mask will be cleared. + // If channel mask is set after channel count, the channel count will be automatically + // calculated from selected channel mask. In that case, only set channel mask when the API + // is available and the channel mask is specified. + if (mLibLoader->builder_setChannelMask != nullptr && mChannelMask != ChannelMask::Unspecified) { + mLibLoader->builder_setChannelMask(aaudioBuilder, + static_cast(mChannelMask)); + } else { + mLibLoader->builder_setChannelCount(aaudioBuilder, mChannelCount); + } + mLibLoader->builder_setDeviceId(aaudioBuilder, getDeviceId()); + mLibLoader->builder_setDirection(aaudioBuilder, static_cast(mDirection)); + mLibLoader->builder_setFormat(aaudioBuilder, static_cast(mFormat)); + mLibLoader->builder_setSampleRate(aaudioBuilder, mSampleRate); + mLibLoader->builder_setSharingMode(aaudioBuilder, + static_cast(mSharingMode)); + mLibLoader->builder_setPerformanceMode(aaudioBuilder, + static_cast(mPerformanceMode)); + + // These were added in P so we have to check for the function pointer. + if (mLibLoader->builder_setUsage != nullptr) { + mLibLoader->builder_setUsage(aaudioBuilder, + static_cast(mUsage)); + } + + if (mLibLoader->builder_setContentType != nullptr) { + mLibLoader->builder_setContentType(aaudioBuilder, + static_cast(mContentType)); + } + + if (mLibLoader->builder_setInputPreset != nullptr) { + aaudio_input_preset_t inputPreset = mInputPreset; + if (getSdkVersion() <= __ANDROID_API_P__ && inputPreset == InputPreset::VoicePerformance) { + LOGD("InputPreset::VoicePerformance not supported before Q. Using VoiceRecognition."); + inputPreset = InputPreset::VoiceRecognition; // most similar preset + } + mLibLoader->builder_setInputPreset(aaudioBuilder, + static_cast(inputPreset)); + } + + // These were added in S so we have to check for the function pointer. + if (mLibLoader->builder_setPackageName != nullptr && !mPackageName.empty()) { + mLibLoader->builder_setPackageName(aaudioBuilder, + mPackageName.c_str()); + } + + if (mLibLoader->builder_setAttributionTag != nullptr && !mAttributionTag.empty()) { + mLibLoader->builder_setAttributionTag(aaudioBuilder, + mAttributionTag.c_str()); + } + + // This was added in Q so we have to check for the function pointer. + if (mLibLoader->builder_setAllowedCapturePolicy != nullptr && mDirection == oboe::Direction::Output) { + mLibLoader->builder_setAllowedCapturePolicy(aaudioBuilder, + static_cast(mAllowedCapturePolicy)); + } + + if (mLibLoader->builder_setPrivacySensitive != nullptr && mDirection == oboe::Direction::Input + && mPrivacySensitiveMode != PrivacySensitiveMode::Unspecified) { + mLibLoader->builder_setPrivacySensitive(aaudioBuilder, + mPrivacySensitiveMode == PrivacySensitiveMode::Enabled); + } + + if (mLibLoader->builder_setIsContentSpatialized != nullptr) { + mLibLoader->builder_setIsContentSpatialized(aaudioBuilder, mIsContentSpatialized); + } + + if (mLibLoader->builder_setSpatializationBehavior != nullptr) { + // Override Unspecified as Never to reduce latency. + if (mSpatializationBehavior == SpatializationBehavior::Unspecified) { + mSpatializationBehavior = SpatializationBehavior::Never; + } + mLibLoader->builder_setSpatializationBehavior(aaudioBuilder, + static_cast(mSpatializationBehavior)); + } else { + mSpatializationBehavior = SpatializationBehavior::Never; + } + + if (anyDataCallbackSpecified()) { + if (isDataCallbackSpecified()) { + mLibLoader->builder_setDataCallback( + aaudioBuilder, oboe_aaudio_data_callback_proc, this); + } else if (isPartialDataCallbackSpecified()) { + if (mLibLoader->builder_setPartialDataCallback == nullptr) { + // This must not happen. The stream should fail open from the builder. + // But having a check here to avoid crashing. + LOGE("Using partial data callback while it is not available"); + return Result::ErrorIllegalArgument; + } + mLibLoader->builder_setPartialDataCallback( + aaudioBuilder, oboe_aaudio_partial_data_callback_proc, this); + } + mLibLoader->builder_setFramesPerDataCallback(aaudioBuilder, getFramesPerDataCallback()); + + if (!isErrorCallbackSpecified()) { + // The app did not specify a callback so we should specify + // our own so the stream gets closed and stopped. + mErrorCallback = &mDefaultErrorCallback; + } + mLibLoader->builder_setErrorCallback(aaudioBuilder, internalErrorCallback, this); + } + // Else if the data callback is not being used then the write method will return an error + // and the app can stop and close the stream. + + if (isPresentationCallbackSpecified() && + mLibLoader->builder_setPresentationEndCallback != nullptr) { + mLibLoader->builder_setPresentationEndCallback(aaudioBuilder, + internalPresentationEndCallback, + this); + } + + // ============= OPEN THE STREAM ================ + { + AAudioStream *stream = nullptr; + result = static_cast(mLibLoader->builder_openStream(aaudioBuilder, &stream)); + mAAudioStream.store(stream); + } + if (result != Result::OK) { + // Warn developer because ErrorInternal is not very informative. + if (result == Result::ErrorInternal && mDirection == Direction::Input) { + LOGW("AudioStreamAAudio.open() may have failed due to lack of " + "audio recording permission."); + } + goto error2; + } + + // Query and cache the stream properties + mChannelCount = mLibLoader->stream_getChannelCount(mAAudioStream); + mSampleRate = mLibLoader->stream_getSampleRate(mAAudioStream); + mFormat = static_cast(mLibLoader->stream_getFormat(mAAudioStream)); + mSharingMode = static_cast(mLibLoader->stream_getSharingMode(mAAudioStream)); + mPerformanceMode = static_cast( + mLibLoader->stream_getPerformanceMode(mAAudioStream)); + mBufferCapacityInFrames = mLibLoader->stream_getBufferCapacity(mAAudioStream); + mBufferSizeInFrames = mLibLoader->stream_getBufferSize(mAAudioStream); + mFramesPerBurst = mLibLoader->stream_getFramesPerBurst(mAAudioStream); + + // These were added in P so we have to check for the function pointer. + if (mLibLoader->stream_getUsage != nullptr) { + mUsage = static_cast(mLibLoader->stream_getUsage(mAAudioStream)); + } + if (mLibLoader->stream_getContentType != nullptr) { + mContentType = static_cast(mLibLoader->stream_getContentType(mAAudioStream)); + } + if (mLibLoader->stream_getInputPreset != nullptr) { + mInputPreset = static_cast(mLibLoader->stream_getInputPreset(mAAudioStream)); + } + if (mLibLoader->stream_getSessionId != nullptr) { + mSessionId = static_cast(mLibLoader->stream_getSessionId(mAAudioStream)); + } else { + mSessionId = SessionId::None; + } + + // This was added in Q so we have to check for the function pointer. + if (mLibLoader->stream_getAllowedCapturePolicy != nullptr && mDirection == oboe::Direction::Output) { + mAllowedCapturePolicy = static_cast(mLibLoader->stream_getAllowedCapturePolicy(mAAudioStream)); + } else { + mAllowedCapturePolicy = AllowedCapturePolicy::Unspecified; + } + + if (mLibLoader->stream_isPrivacySensitive != nullptr && mDirection == oboe::Direction::Input) { + bool isPrivacySensitive = mLibLoader->stream_isPrivacySensitive(mAAudioStream); + mPrivacySensitiveMode = isPrivacySensitive ? PrivacySensitiveMode::Enabled : + PrivacySensitiveMode::Disabled; + } else { + mPrivacySensitiveMode = PrivacySensitiveMode::Unspecified; + } + + if (mLibLoader->stream_getChannelMask != nullptr) { + mChannelMask = static_cast(mLibLoader->stream_getChannelMask(mAAudioStream)); + } + + if (mLibLoader->stream_isContentSpatialized != nullptr) { + mIsContentSpatialized = mLibLoader->stream_isContentSpatialized(mAAudioStream); + } + + if (mLibLoader->stream_getSpatializationBehavior != nullptr) { + mSpatializationBehavior = static_cast( + mLibLoader->stream_getSpatializationBehavior(mAAudioStream)); + } + + if (mLibLoader->stream_getHardwareChannelCount != nullptr) { + mHardwareChannelCount = mLibLoader->stream_getHardwareChannelCount(mAAudioStream); + } + if (mLibLoader->stream_getHardwareSampleRate != nullptr) { + mHardwareSampleRate = mLibLoader->stream_getHardwareSampleRate(mAAudioStream); + } + if (mLibLoader->stream_getHardwareFormat != nullptr) { + mHardwareFormat = static_cast(mLibLoader->stream_getHardwareFormat(mAAudioStream)); + } + + updateDeviceIds(); + + LOGD("AudioStreamAAudio.open() format=%d, sampleRate=%d, capacity = %d", + static_cast(mFormat), static_cast(mSampleRate), + static_cast(mBufferCapacityInFrames)); + + calculateDefaultDelayBeforeCloseMillis(); + +error2: + mLibLoader->builder_delete(aaudioBuilder); + if (static_cast(result) > 0) { + // Possibly due to b/267531411 + LOGW("AudioStreamAAudio.open: AAudioStream_Open() returned positive error = %d", + static_cast(result)); + if (OboeGlobals::areWorkaroundsEnabled()) { + result = Result::ErrorInternal; // Coerce to negative error. + } + } else { + LOGD("AudioStreamAAudio.open: AAudioStream_Open() returned %s = %d", + mLibLoader->convertResultToText(static_cast(result)), + static_cast(result)); + } + return result; +} + +Result AudioStreamAAudio::release() { + if (getSdkVersion() < __ANDROID_API_R__) { + return Result::ErrorUnimplemented; + } + + // AAudioStream_release() is buggy on Android R. + if (OboeGlobals::areWorkaroundsEnabled() && getSdkVersion() == __ANDROID_API_R__) { + LOGW("Skipping release() on Android R"); + return Result::ErrorUnimplemented; + } + + std::lock_guard lock(mLock); + AAudioStream *stream = mAAudioStream.load(); + if (stream != nullptr) { + if (OboeGlobals::areWorkaroundsEnabled()) { + // Make sure we are really stopped. Do it under mLock + // so another thread cannot call requestStart() right before the close. + requestStop_l(stream); + } + return static_cast(mLibLoader->stream_release(stream)); + } else { + return Result::ErrorClosed; + } +} + +Result AudioStreamAAudio::close() { + // Prevent two threads from closing the stream at the same time and crashing. + // This could occur, for example, if an application called close() at the same + // time that an onError callback was being executed because of a disconnect. + std::lock_guard lock(mLock); + + AudioStream::close(); + + AAudioStream *stream = nullptr; + { + // Wait for any methods using mAAudioStream to finish. + std::unique_lock lock2(mAAudioStreamLock); + // Closing will delete *mAAudioStream so we need to null out the pointer atomically. + stream = mAAudioStream.exchange(nullptr); + } + if (stream != nullptr) { + if (OboeGlobals::areWorkaroundsEnabled()) { + // Make sure we are really stopped. Do it under mLock + // so another thread cannot call requestStart() right before the close. + requestStop_l(stream); + sleepBeforeClose(); + } + return static_cast(mLibLoader->stream_close(stream)); + } else { + return Result::ErrorClosed; + } +} + +static void oboe_stop_thread_proc(AudioStream *oboeStream) { + if (oboeStream != nullptr) { + oboeStream->requestStop(); + } +} + +void AudioStreamAAudio::launchStopThread() { + // Prevent multiple stop threads from being launched. + if (mStopThreadAllowed.exchange(false)) { + // Stop this stream on a separate thread + std::thread t(oboe_stop_thread_proc, this); + t.detach(); + } +} + +DataCallbackResult AudioStreamAAudio::callOnAudioReady(AAudioStream * /*stream*/, + void *audioData, + int32_t numFrames) { + DataCallbackResult result = fireDataCallback(audioData, numFrames); + if (result == DataCallbackResult::Continue) { + return result; + } else { + if (result == DataCallbackResult::Stop) { + LOGD("Oboe callback returned DataCallbackResult::Stop"); + } else { + LOGE("Oboe callback returned unexpected value. Error: %d", static_cast(result)); + } + + // Returning Stop caused various problems before S. See #1230 + if (OboeGlobals::areWorkaroundsEnabled() && getSdkVersion() <= __ANDROID_API_R__) { + launchStopThread(); + return DataCallbackResult::Continue; + } else { + return DataCallbackResult::Stop; // OK >= API_S + } + } +} + +int32_t AudioStreamAAudio::callOnPartialAudioReady(AAudioStream * /*stream*/, + void *audioData, + int32_t numFrames) { + return firePartialDataCallback(audioData, numFrames); +} + +Result AudioStreamAAudio::requestStart() { + std::lock_guard lock(mLock); + AAudioStream *stream = mAAudioStream.load(); + if (stream != nullptr) { + // Avoid state machine errors in O_MR1. + if (getSdkVersion() <= __ANDROID_API_O_MR1__) { + StreamState state = static_cast(mLibLoader->stream_getState(stream)); + if (state == StreamState::Starting || state == StreamState::Started) { + // WARNING: On P, AAudio is returning ErrorInvalidState for Output and OK for Input. + return Result::OK; + } + } + if (anyDataCallbackSpecified()) { + setDataCallbackEnabled(true); + } + mStopThreadAllowed = true; + closePerformanceHint(); + return static_cast(mLibLoader->stream_requestStart(stream)); + } else { + return Result::ErrorClosed; + } +} + +Result AudioStreamAAudio::requestPause() { + std::lock_guard lock(mLock); + AAudioStream *stream = mAAudioStream.load(); + if (stream != nullptr) { + // Avoid state machine errors in O_MR1. + if (getSdkVersion() <= __ANDROID_API_O_MR1__) { + StreamState state = static_cast(mLibLoader->stream_getState(stream)); + if (state == StreamState::Pausing || state == StreamState::Paused) { + return Result::OK; + } + } + return static_cast(mLibLoader->stream_requestPause(stream)); + } else { + return Result::ErrorClosed; + } +} + +Result AudioStreamAAudio::requestFlush() { + std::lock_guard lock(mLock); + AAudioStream *stream = mAAudioStream.load(); + if (stream != nullptr) { + // Avoid state machine errors in O_MR1. + if (getSdkVersion() <= __ANDROID_API_O_MR1__) { + StreamState state = static_cast(mLibLoader->stream_getState(stream)); + if (state == StreamState::Flushing || state == StreamState::Flushed) { + return Result::OK; + } + } + return static_cast(mLibLoader->stream_requestFlush(stream)); + } else { + return Result::ErrorClosed; + } +} + +Result AudioStreamAAudio::requestStop() { + std::lock_guard lock(mLock); + AAudioStream *stream = mAAudioStream.load(); + if (stream != nullptr) { + return requestStop_l(stream); + } else { + return Result::ErrorClosed; + } +} + +// Call under mLock +Result AudioStreamAAudio::requestStop_l(AAudioStream *stream) { + // Avoid state machine errors in O_MR1. + if (getSdkVersion() <= __ANDROID_API_O_MR1__) { + StreamState state = static_cast(mLibLoader->stream_getState(stream)); + if (state == StreamState::Stopping || state == StreamState::Stopped) { + return Result::OK; + } + } + return static_cast(mLibLoader->stream_requestStop(stream)); +} + +ResultWithValue AudioStreamAAudio::write(const void *buffer, + int32_t numFrames, + int64_t timeoutNanoseconds) { + std::shared_lock lock(mAAudioStreamLock); + AAudioStream *stream = mAAudioStream.load(); + if (stream != nullptr) { + int32_t result = mLibLoader->stream_write(mAAudioStream, buffer, + numFrames, timeoutNanoseconds); + return ResultWithValue::createBasedOnSign(result); + } else { + return ResultWithValue(Result::ErrorClosed); + } +} + +ResultWithValue AudioStreamAAudio::read(void *buffer, + int32_t numFrames, + int64_t timeoutNanoseconds) { + std::shared_lock lock(mAAudioStreamLock); + AAudioStream *stream = mAAudioStream.load(); + if (stream != nullptr) { + int32_t result = mLibLoader->stream_read(mAAudioStream, buffer, + numFrames, timeoutNanoseconds); + return ResultWithValue::createBasedOnSign(result); + } else { + return ResultWithValue(Result::ErrorClosed); + } +} + + +// AAudioStream_waitForStateChange() can crash if it is waiting on a stream and that stream +// is closed from another thread. We do not want to lock the stream for the duration of the call. +// So we call AAudioStream_waitForStateChange() with a timeout of zero so that it will not block. +// Then we can do our own sleep with the lock unlocked. +Result AudioStreamAAudio::waitForStateChange(StreamState currentState, + StreamState *nextState, + int64_t timeoutNanoseconds) { + Result oboeResult = Result::ErrorTimeout; + int64_t sleepTimeNanos = 20 * kNanosPerMillisecond; // arbitrary + aaudio_stream_state_t currentAAudioState = static_cast(currentState); + + aaudio_result_t result = AAUDIO_OK; + int64_t timeLeftNanos = timeoutNanoseconds; + + mLock.lock(); + while (true) { + // Do we still have an AAudio stream? If not then stream must have been closed. + AAudioStream *stream = mAAudioStream.load(); + if (stream == nullptr) { + if (nextState != nullptr) { + *nextState = StreamState::Closed; + } + oboeResult = Result::ErrorClosed; + break; + } + + // Update and query state change with no blocking. + aaudio_stream_state_t aaudioNextState; + result = mLibLoader->stream_waitForStateChange( + mAAudioStream, + currentAAudioState, + &aaudioNextState, + 0); // timeout=0 for non-blocking + // AAudio will return AAUDIO_ERROR_TIMEOUT if timeout=0 and the state does not change. + if (result != AAUDIO_OK && result != AAUDIO_ERROR_TIMEOUT) { + oboeResult = static_cast(result); + break; + } +#if OBOE_FIX_FORCE_STARTING_TO_STARTED + if (OboeGlobals::areWorkaroundsEnabled() + && aaudioNextState == static_cast(StreamState::Starting)) { + aaudioNextState = static_cast(StreamState::Started); + } +#endif // OBOE_FIX_FORCE_STARTING_TO_STARTED + if (nextState != nullptr) { + *nextState = static_cast(aaudioNextState); + } + if (currentAAudioState != aaudioNextState) { // state changed? + oboeResult = Result::OK; + break; + } + + // Did we timeout or did user ask for non-blocking? + if (timeLeftNanos <= 0) { + break; + } + + // No change yet so sleep. + mLock.unlock(); // Don't sleep while locked. + if (sleepTimeNanos > timeLeftNanos) { + sleepTimeNanos = timeLeftNanos; // last little bit + } + AudioClock::sleepForNanos(sleepTimeNanos); + timeLeftNanos -= sleepTimeNanos; + mLock.lock(); + } + + mLock.unlock(); + return oboeResult; +} + +ResultWithValue AudioStreamAAudio::setBufferSizeInFrames(int32_t requestedFrames) { + int32_t adjustedFrames = requestedFrames; + if (adjustedFrames > mBufferCapacityInFrames) { + adjustedFrames = mBufferCapacityInFrames; + } + // This calls getBufferSize() so avoid recursive lock. + adjustedFrames = QuirksManager::getInstance().clipBufferSize(*this, adjustedFrames); + + std::shared_lock lock(mAAudioStreamLock); + AAudioStream *stream = mAAudioStream.load(); + if (stream != nullptr) { + int32_t newBufferSize = mLibLoader->stream_setBufferSize(mAAudioStream, adjustedFrames); + // Cache the result if it's valid + if (newBufferSize > 0) mBufferSizeInFrames = newBufferSize; + return ResultWithValue::createBasedOnSign(newBufferSize); + } else { + return ResultWithValue(Result::ErrorClosed); + } +} + +StreamState AudioStreamAAudio::getState() { + std::shared_lock lock(mAAudioStreamLock); + AAudioStream *stream = mAAudioStream.load(); + if (stream != nullptr) { + aaudio_stream_state_t aaudioState = mLibLoader->stream_getState(stream); +#if OBOE_FIX_FORCE_STARTING_TO_STARTED + if (OboeGlobals::areWorkaroundsEnabled() + && aaudioState == AAUDIO_STREAM_STATE_STARTING) { + aaudioState = AAUDIO_STREAM_STATE_STARTED; + } +#endif // OBOE_FIX_FORCE_STARTING_TO_STARTED + return static_cast(aaudioState); + } else { + return StreamState::Closed; + } +} + +int32_t AudioStreamAAudio::getBufferSizeInFrames() { + std::shared_lock lock(mAAudioStreamLock); + AAudioStream *stream = mAAudioStream.load(); + if (stream != nullptr) { + mBufferSizeInFrames = mLibLoader->stream_getBufferSize(stream); + } + return mBufferSizeInFrames; +} + +void AudioStreamAAudio::updateFramesRead() { + std::shared_lock lock(mAAudioStreamLock); + AAudioStream *stream = mAAudioStream.load(); +// Set to 1 for debugging race condition #1180 with mAAudioStream. +// See also DEBUG_CLOSE_RACE in OboeTester. +// This was left in the code so that we could test the fix again easily in the future. +// We could not trigger the race condition without adding these get calls and the sleeps. +#define DEBUG_CLOSE_RACE 0 +#if DEBUG_CLOSE_RACE + // This is used when testing race conditions with close(). + // See DEBUG_CLOSE_RACE in OboeTester + AudioClock::sleepForNanos(400 * kNanosPerMillisecond); +#endif // DEBUG_CLOSE_RACE + if (stream != nullptr) { + mFramesRead = mLibLoader->stream_getFramesRead(stream); + } +} + +void AudioStreamAAudio::updateFramesWritten() { + std::shared_lock lock(mAAudioStreamLock); + AAudioStream *stream = mAAudioStream.load(); + if (stream != nullptr) { + mFramesWritten = mLibLoader->stream_getFramesWritten(stream); + } +} + +ResultWithValue AudioStreamAAudio::getXRunCount() { + std::shared_lock lock(mAAudioStreamLock); + AAudioStream *stream = mAAudioStream.load(); + if (stream != nullptr) { + return ResultWithValue::createBasedOnSign(mLibLoader->stream_getXRunCount(stream)); + } else { + return ResultWithValue(Result::ErrorNull); + } +} + +Result AudioStreamAAudio::getTimestamp(clockid_t clockId, + int64_t *framePosition, + int64_t *timeNanoseconds) { + if (getState() != StreamState::Started) { + return Result::ErrorInvalidState; + } + std::shared_lock lock(mAAudioStreamLock); + AAudioStream *stream = mAAudioStream.load(); + if (stream != nullptr) { + return static_cast(mLibLoader->stream_getTimestamp(stream, clockId, + framePosition, timeNanoseconds)); + } else { + return Result::ErrorNull; + } +} + +ResultWithValue AudioStreamAAudio::calculateLatencyMillis() { + // Get the time that a known audio frame was presented. + int64_t hardwareFrameIndex; + int64_t hardwareFrameHardwareTime; + auto result = getTimestamp(CLOCK_MONOTONIC, + &hardwareFrameIndex, + &hardwareFrameHardwareTime); + if (result != oboe::Result::OK) { + return ResultWithValue(static_cast(result)); + } + + // Get counter closest to the app. + bool isOutput = (getDirection() == oboe::Direction::Output); + int64_t appFrameIndex = isOutput ? getFramesWritten() : getFramesRead(); + + // Assume that the next frame will be processed at the current time + using namespace std::chrono; + int64_t appFrameAppTime = + duration_cast(steady_clock::now().time_since_epoch()).count(); + + // Calculate the number of frames between app and hardware + int64_t frameIndexDelta = appFrameIndex - hardwareFrameIndex; + + // Calculate the time which the next frame will be or was presented + int64_t frameTimeDelta = (frameIndexDelta * oboe::kNanosPerSecond) / getSampleRate(); + int64_t appFrameHardwareTime = hardwareFrameHardwareTime + frameTimeDelta; + + // The current latency is the difference in time between when the current frame is at + // the app and when it is at the hardware. + double latencyNanos = static_cast(isOutput + ? (appFrameHardwareTime - appFrameAppTime) // hardware is later + : (appFrameAppTime - appFrameHardwareTime)); // hardware is earlier + double latencyMillis = latencyNanos / kNanosPerMillisecond; + + return ResultWithValue(latencyMillis); +} + +bool AudioStreamAAudio::isMMapUsed() { + std::shared_lock lock(mAAudioStreamLock); + AAudioStream *stream = mAAudioStream.load(); + if (stream != nullptr) { + return AAudioExtensions::getInstance().isMMapUsed(stream); + } else { + return false; + } +} + +// static +// Static method for the presentation end callback. +// We use a method so we can access protected methods on the stream. +// Launch a thread to handle the error. +// That other thread can safely stop, close and delete the stream. +void AudioStreamAAudio::internalPresentationEndCallback(AAudioStream *stream, void *userData) { + AudioStreamAAudio *oboeStream = reinterpret_cast(userData); + + // Prevents deletion of the stream if the app is using AudioStreamBuilder::openStream(shared_ptr) + std::shared_ptr sharedStream = oboeStream->lockWeakThis(); + + if (stream != oboeStream->getUnderlyingStream()) { + LOGW("%s() stream already closed or closing", __func__); // might happen if there are bugs + } else if (sharedStream) { + // Handle error on a separate thread using shared pointer. + std::thread t(oboe_aaudio_presentation_end_thread_proc_shared, sharedStream); + t.detach(); + } else { + // Handle error on a separate thread. + std::thread t(oboe_aaudio_presentation_thread_proc, oboeStream); + t.detach(); + } +} + +Result AudioStreamAAudio::setOffloadDelayPadding( + int32_t delayInFrames, int32_t paddingInFrames) { + if (mLibLoader->stream_setOffloadDelayPadding == nullptr) { + return Result::ErrorUnimplemented; + } + std::shared_lock lock(mAAudioStreamLock); + AAudioStream *stream = mAAudioStream.load(); + if (stream == nullptr) { + return Result::ErrorClosed; + } + return static_cast( + mLibLoader->stream_setOffloadDelayPadding(stream, delayInFrames, paddingInFrames)); +} + +ResultWithValue AudioStreamAAudio::getOffloadDelay() { + if (mLibLoader->stream_getOffloadDelay == nullptr) { + return ResultWithValue(Result::ErrorUnimplemented); + } + std::shared_lock lock(mAAudioStreamLock); + AAudioStream *stream = mAAudioStream.load(); + if (stream == nullptr) { + return Result::ErrorClosed; + } + return ResultWithValue::createBasedOnSign(mLibLoader->stream_getOffloadDelay(stream)); +} + +ResultWithValue AudioStreamAAudio::getOffloadPadding() { + if (mLibLoader->stream_getOffloadPadding == nullptr) { + return ResultWithValue(Result::ErrorUnimplemented); + } + std::shared_lock lock(mAAudioStreamLock); + AAudioStream *stream = mAAudioStream.load(); + if (stream == nullptr) { + return ResultWithValue(Result::ErrorClosed); + } + return ResultWithValue::createBasedOnSign( + mLibLoader->stream_getOffloadPadding(stream)); +} + +Result AudioStreamAAudio::setOffloadEndOfStream() { + if (mLibLoader->stream_setOffloadEndOfStream == nullptr) { + return Result::ErrorUnimplemented; + } + std::shared_lock lock(mAAudioStreamLock); + AAudioStream *stream = mAAudioStream.load(); + if (stream == nullptr) { + return ResultWithValue(Result::ErrorClosed); + } + return static_cast(mLibLoader->stream_setOffloadEndOfStream(stream)); +} + +void AudioStreamAAudio::updateDeviceIds() { + // If stream_getDeviceIds is not supported, use stream_getDeviceId. + if (mLibLoader->stream_getDeviceIds == nullptr) { + mDeviceIds.clear(); + int32_t deviceId = mLibLoader->stream_getDeviceId(mAAudioStream); + if (deviceId != kUnspecified) { + mDeviceIds.push_back(deviceId); + } + } else { + // Allocate a temp vector with 16 elements. This should be enough to cover all cases. + // Please file a bug on Oboe if you discover that this returns AAUDIO_ERROR_OUT_OF_RANGE. + // When AAUDIO_ERROR_OUT_OF_RANGE is returned, the actual size will be still returned as the + // value of deviceIdSize but deviceIds will be empty. + + static constexpr int kDefaultDeviceIdSize = 16; + int deviceIdSize = kDefaultDeviceIdSize; + std::vector deviceIds(deviceIdSize); + aaudio_result_t getDeviceIdResult = + mLibLoader->stream_getDeviceIds(mAAudioStream, deviceIds.data(), &deviceIdSize); + if (getDeviceIdResult != AAUDIO_OK) { + LOGE("stream_getDeviceIds did not return AAUDIO_OK. Error: %d", + static_cast(getDeviceIdResult)); + return; + } + + mDeviceIds.clear(); + for (int i = 0; i < deviceIdSize; i++) { + mDeviceIds.push_back(deviceIds[i]); + } + } + + // This should not happen in most cases. Please file a bug on Oboe if you see this happening. + if (mDeviceIds.empty()) { + LOGW("updateDeviceIds() returns an empty array."); + } +} + +ResultWithValue AudioStreamAAudio::flushFromFrame(FlushFromAccuracy accuracy, + int64_t positionInFrames) { + if (mLibLoader->stream_flushFromFrame == nullptr) { + return ResultWithValue(positionInFrames, Result::ErrorUnimplemented); + } + std::shared_lock lock(mAAudioStreamLock); + AAudioStream *stream = mAAudioStream.load(); + if (stream == nullptr) { + return ResultWithValue(positionInFrames, Result::ErrorClosed); + } + // TODO: use aaudio_flush_from_frame_accuracy_t when it is defined. + auto result = static_cast(mLibLoader->stream_flushFromFrame( + stream, static_cast(accuracy), &positionInFrames)); + return ResultWithValue(positionInFrames, result); +} + +namespace { + +ResultWithValue oboe2AAudio_PlaybackParameters_AAudioPlaybackParameters( + const PlaybackParameters& playbackParameters) { + AAudioPlaybackParameters aaudioPlaybackParameters; + switch (playbackParameters.fallbackMode) { + case FallbackMode::Default: + aaudioPlaybackParameters.fallbackMode = AAUDIO_FALLBACK_MODE_DEFAULT; + break; + case FallbackMode::Mute: + aaudioPlaybackParameters.fallbackMode = AAUDIO_FALLBACK_MODE_MUTE; + break; + case FallbackMode::Fail: + aaudioPlaybackParameters.fallbackMode = AAUDIO_FALLBACK_MODE_FAIL; + break; + default: + return ResultWithValue(Result::ErrorIllegalArgument); + } + + switch (playbackParameters.stretchMode) { + case StretchMode::Default: + aaudioPlaybackParameters.stretchMode = AAUDIO_STRETCH_MODE_DEFAULT; + break; + case StretchMode::Voice: + aaudioPlaybackParameters.stretchMode = AAUDIO_STRETCH_MODE_VOICE; + break; + default: + return ResultWithValue(Result::ErrorIllegalArgument); + } + + aaudioPlaybackParameters.pitch = playbackParameters.pitch; + aaudioPlaybackParameters.speed = playbackParameters.speed; + return ResultWithValue(aaudioPlaybackParameters); +} + +ResultWithValue aaudio2oboe_AAudioPlaybackParameters_PlaybackParameters( + const AAudioPlaybackParameters& aaudioPlaybackParameters) { + PlaybackParameters playbackParameters; + switch (aaudioPlaybackParameters.fallbackMode) { + case AAUDIO_FALLBACK_MODE_DEFAULT: + playbackParameters.fallbackMode = FallbackMode::Default; + break; + case AAUDIO_FALLBACK_MODE_MUTE: + playbackParameters.fallbackMode = FallbackMode::Mute; + break; + case AAUDIO_FALLBACK_MODE_FAIL: + playbackParameters.fallbackMode = FallbackMode::Fail; + break; + default: + LOGE("%s unknown fallback mode %d", __func__, aaudioPlaybackParameters.fallbackMode); + return ResultWithValue(Result::ErrorIllegalArgument); + } + + switch (aaudioPlaybackParameters.stretchMode) { + case AAUDIO_STRETCH_MODE_DEFAULT: + playbackParameters.stretchMode = StretchMode::Default; + break; + case AAUDIO_STRETCH_MODE_VOICE: + playbackParameters.stretchMode = StretchMode::Voice; + break; + default: + LOGE("%s unknown stretch mode %d", __func__, aaudioPlaybackParameters.stretchMode); + return ResultWithValue(Result::ErrorIllegalArgument); + } + playbackParameters.pitch = aaudioPlaybackParameters.pitch; + playbackParameters.speed = aaudioPlaybackParameters.speed; + return ResultWithValue(playbackParameters); +} + +} // namespace + +Result AudioStreamAAudio::setPlaybackParameters(const PlaybackParameters ¶meters) { + if (mLibLoader->stream_setPlaybackParameters == nullptr) { + LOGD("%s, the NDK function is not available", __func__); + return Result::ErrorUnimplemented; + } + std::shared_lock _l(mAAudioStreamLock); + AAudioStream *stream = mAAudioStream.load(); + if (stream == nullptr) { + LOGE("%s the stream is already closed", __func__); + return Result::ErrorClosed; + } + auto convertResult = + oboe2AAudio_PlaybackParameters_AAudioPlaybackParameters(parameters); + if (!convertResult) { + LOGE("%s, invalid parameters, %s", __func__, toString(parameters).c_str()); + return Result::ErrorIllegalArgument; + } + auto aaudioPlaybackParameters = convertResult.value(); + return static_cast(mLibLoader->stream_setPlaybackParameters( + stream, &aaudioPlaybackParameters)); +} + +ResultWithValue AudioStreamAAudio::getPlaybackParameters() { + if (mLibLoader->stream_getPlaybackParameters == nullptr) { + LOGD("%s, the NDK function is not available", __func__); + return Result::ErrorUnimplemented; + } + std::shared_lock _l(mAAudioStreamLock); + AAudioStream *stream = mAAudioStream.load(); + if (stream == nullptr) { + LOGE("%s the stream is already closed", __func__); + return Result::ErrorClosed; + } + + AAudioPlaybackParameters aaudioPlaybackParameters; + auto result = static_cast( + mLibLoader->stream_getPlaybackParameters(stream, &aaudioPlaybackParameters)); + if (result != Result::OK) { + return ResultWithValue(result); + } + + return aaudio2oboe_AAudioPlaybackParameters_PlaybackParameters(aaudioPlaybackParameters); +} + +} // namespace oboe diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_aaudio_AudioStreamAAudio_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_aaudio_AudioStreamAAudio_android.h new file mode 100644 index 0000000..b60a42c --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_aaudio_AudioStreamAAudio_android.h @@ -0,0 +1,198 @@ +/* + * Copyright 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_STREAM_AAUDIO_H_ +#define OBOE_STREAM_AAUDIO_H_ + +#include +#include +#include +#include + +#include "oboe_common_AdpfWrapper_android.h" +#include "oboe_oboe_AudioStreamBuilder_android.h" +#include "oboe_oboe_AudioStream_android.h" +#include "oboe_oboe_Definitions_android.h" +#include "oboe_aaudio_AAudioLoader_android.h" + +namespace oboe { + +/** + * Implementation of OboeStream that uses AAudio. + * + * Do not create this class directly. + * Use an OboeStreamBuilder to create one. + */ +class AudioStreamAAudio : public AudioStream { +public: + AudioStreamAAudio(); + explicit AudioStreamAAudio(const AudioStreamBuilder &builder); + + virtual ~AudioStreamAAudio() = default; + + /** + * + * @return true if AAudio is supported on this device. + */ + static bool isSupported(); + + // These functions override methods in AudioStream. + // See AudioStream for documentation. + Result open() override; + Result release() override; + Result close() override; + + Result requestStart() override; + Result requestPause() override; + Result requestFlush() override; + Result requestStop() override; + + ResultWithValue write(const void *buffer, + int32_t numFrames, + int64_t timeoutNanoseconds) override; + + ResultWithValue read(void *buffer, + int32_t numFrames, + int64_t timeoutNanoseconds) override; + + ResultWithValue setBufferSizeInFrames(int32_t requestedFrames) override; + int32_t getBufferSizeInFrames() override; + ResultWithValue getXRunCount() override; + bool isXRunCountSupported() const override { return true; } + + ResultWithValue calculateLatencyMillis() override; + + Result waitForStateChange(StreamState currentState, + StreamState *nextState, + int64_t timeoutNanoseconds) override; + + Result getTimestamp(clockid_t clockId, + int64_t *framePosition, + int64_t *timeNanoseconds) override; + + StreamState getState() override; + + AudioApi getAudioApi() const override { + return AudioApi::AAudio; + } + + DataCallbackResult callOnAudioReady(AAudioStream *stream, void *audioData, int32_t numFrames); + + int32_t callOnPartialAudioReady(AAudioStream *stream, void *audioData, int32_t numFrames); + + bool isMMapUsed(); + + void closePerformanceHint() override { + mAdpfWrapper.close(); + mAdpfOpenAttempted = false; + } + + oboe::Result reportWorkload(int32_t appWorkload) override { + if (!isPerformanceHintEnabled()) { + return oboe::Result::ErrorInvalidState; + } + mAdpfWrapper.reportWorkload(appWorkload); + return oboe::Result::OK; + } + + oboe::Result notifyWorkloadIncrease(bool cpu, bool gpu, const char* debugName) override { + if (!isPerformanceHintEnabled()) { + return oboe::Result::ErrorInvalidState; + } + return mAdpfWrapper.notifyWorkloadIncrease(cpu, gpu, debugName); + } + + oboe::Result notifyWorkloadSpike(bool cpu, bool gpu, const char* debugName) override { + if (!isPerformanceHintEnabled()) { + return oboe::Result::ErrorInvalidState; + } + return mAdpfWrapper.notifyWorkloadSpike(cpu, gpu, debugName); + } + + oboe::Result notifyWorkloadReset(bool cpu, bool gpu, const char* debugName) override { + if (!isPerformanceHintEnabled()) { + return oboe::Result::ErrorInvalidState; + } + return mAdpfWrapper.notifyWorkloadReset(cpu, gpu, debugName); + } + + Result setOffloadDelayPadding(int32_t delayInFrames, int32_t paddingInFrames) override; + ResultWithValue getOffloadDelay() override; + ResultWithValue getOffloadPadding() override; + Result setOffloadEndOfStream() override; + + ResultWithValue flushFromFrame( + FlushFromAccuracy accuracy, int64_t positionInFrames) override; + + oboe::Result setPlaybackParameters(const PlaybackParameters& parameters) override; + ResultWithValue getPlaybackParameters() override; + +protected: + static void internalErrorCallback( + AAudioStream *stream, + void *userData, + aaudio_result_t error); + + static void internalPresentationEndCallback( + AAudioStream *stream, + void *userData); + + void *getUnderlyingStream() const override { + return mAAudioStream.load(); + } + + void updateFramesRead() override; + void updateFramesWritten() override; + + void logUnsupportedAttributes(); + + void beginPerformanceHintInCallback() override; + + void endPerformanceHintInCallback(int32_t numFrames) override; + + // set by callback (or app when idle) + std::atomic mAdpfOpenAttempted{false}; + AdpfWrapper mAdpfWrapper; + +private: + // Must call under mLock. And stream must NOT be nullptr. + Result requestStop_l(AAudioStream *stream); + + /** + * Launch a thread that will stop the stream. + */ + void launchStopThread(); + + void updateDeviceIds(); + +private: + + std::atomic mCallbackThreadEnabled; + std::atomic mStopThreadAllowed{false}; + + // pointer to the underlying 'C' AAudio stream, valid if open, null if closed + std::atomic mAAudioStream{nullptr}; + std::shared_mutex mAAudioStreamLock; // to protect mAAudioStream while closing + + static AAudioLoader *mLibLoader; + + // We may not use this but it is so small that it is not worth allocating dynamically. + AudioStreamErrorCallback mDefaultErrorCallback; +}; + +} // namespace oboe + +#endif // OBOE_STREAM_AAUDIO_H_ diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_AdpfWrapper_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_AdpfWrapper_android.cpp new file mode 100644 index 0000000..1574dbb --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_AdpfWrapper_android.cpp @@ -0,0 +1,277 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include "oboe_oboe_AudioClock_android.h" +#include "oboe_oboe_Definitions_android.h" +#include "oboe_oboe_Utilities_android.h" +#include "oboe_common_AdpfWrapper_android.h" +#include "oboe_common_OboeDebug_android.h" +#include "oboe_common_Trace_android.h" + +using namespace oboe; + +typedef APerformanceHintManager* (*APH_getManager)(); +typedef APerformanceHintSession* (*APH_createSession)(APerformanceHintManager*, const int32_t*, + size_t, int64_t); +typedef void (*APH_reportActualWorkDuration)(APerformanceHintSession*, int64_t); +typedef void (*APH_closeSession)(APerformanceHintSession* session); +typedef int (*APH_notifyWorkloadIncrease)(APerformanceHintSession*, bool, bool, const char*); +typedef int (*APH_notifyWorkloadSpike)(APerformanceHintSession*, bool, bool, const char*); +typedef int (*APH_notifyWorkloadReset)(APerformanceHintSession*, bool, bool, const char*); + +static bool gAPerformanceHintBindingInitialized = false; +static APH_getManager gAPH_getManagerFn = nullptr; +static APH_createSession gAPH_createSessionFn = nullptr; +static APH_reportActualWorkDuration gAPH_reportActualWorkDurationFn = nullptr; +static APH_closeSession gAPH_closeSessionFn = nullptr; +static APH_notifyWorkloadIncrease gAPH_notifyWorkloadIncreaseFn = nullptr; +static APH_notifyWorkloadSpike gAPH_notifyWorkloadSpikeFn = nullptr; +static APH_notifyWorkloadReset gAPH_notifyWorkloadResetFn = nullptr; + +#ifndef __ANDROID_API_B__ +#define __ANDROID_API_B__ 36 +#endif + +static int loadAphFunctions() { + if (gAPerformanceHintBindingInitialized) return true; + + void* handle_ = dlopen("libandroid.so", RTLD_NOW | RTLD_NODELETE); + if (handle_ == nullptr) { + return -1000; + } + + gAPH_getManagerFn = (APH_getManager)dlsym(handle_, "APerformanceHint_getManager"); + if (gAPH_getManagerFn == nullptr) { + return -1001; + } + + gAPH_createSessionFn = (APH_createSession)dlsym(handle_, "APerformanceHint_createSession"); + if (gAPH_createSessionFn == nullptr) { + return -1002; + } + + gAPH_reportActualWorkDurationFn = (APH_reportActualWorkDuration)dlsym( + handle_, "APerformanceHint_reportActualWorkDuration"); + if (gAPH_reportActualWorkDurationFn == nullptr) { + return -1003; + } + + gAPH_closeSessionFn = (APH_closeSession)dlsym(handle_, "APerformanceHint_closeSession"); + if (gAPH_closeSessionFn == nullptr) { + return -1004; + } + + // TODO: Remove pre-release check after Android B release + if (getSdkVersion() >= __ANDROID_API_B__ || isAtLeastPreReleaseCodename("Baklava")) { + gAPH_notifyWorkloadIncreaseFn = (APH_notifyWorkloadIncrease)dlsym( + handle_, "APerformanceHint_notifyWorkloadIncrease"); + if (gAPH_notifyWorkloadIncreaseFn == nullptr) { + return -1005; + } + gAPH_notifyWorkloadSpikeFn = (APH_notifyWorkloadSpike)dlsym( + handle_, "APerformanceHint_notifyWorkloadSpike"); + if (gAPH_notifyWorkloadSpikeFn == nullptr) { + return -1006; + } + gAPH_notifyWorkloadResetFn = (APH_notifyWorkloadReset)dlsym( + handle_, "APerformanceHint_notifyWorkloadReset"); + if (gAPH_notifyWorkloadResetFn == nullptr) { + return -1007; + } + } + + gAPerformanceHintBindingInitialized = true; + + return 0; +} + +bool AdpfWrapper::sUseAlternativeHack = false; // TODO remove hack + +int AdpfWrapper::open(pid_t threadId, + int64_t targetDurationNanos) { + std::lock_guard lock(mLock); + int result = loadAphFunctions(); + if (result < 0) return result; + + // This is a singleton. + APerformanceHintManager* manager = gAPH_getManagerFn(); + + int32_t thread32 = threadId; + if (sUseAlternativeHack) { + // TODO Remove this hack when we finish experimenting with alternative algorithms. + // The A5 is an arbitrary signal to a hacked version of ADPF to try an alternative + // algorithm that is not based on PID. + targetDurationNanos = (targetDurationNanos & ~0xFF) | 0xA5; + } + mHintSession = gAPH_createSessionFn(manager, &thread32, 1 /* size */, targetDurationNanos); + if (mHintSession == nullptr) { + return -1; + } + return 0; +} + +void AdpfWrapper::reportActualDuration(int64_t actualDurationNanos) { + //LOGD("ADPF Oboe %s(dur=%lld)", __func__, (long long)actualDurationNanos); + std::lock_guard lock(mLock); + if (mHintSession != nullptr) { + bool traceEnabled = Trace::getInstance().isEnabled(); + if (traceEnabled) { + Trace::getInstance().beginSection("reportActualDuration"); + Trace::getInstance().setCounter("actualDurationNanos", actualDurationNanos); + } + gAPH_reportActualWorkDurationFn(mHintSession, actualDurationNanos); + if (traceEnabled) { + Trace::getInstance().endSection(); + } + } +} + +void AdpfWrapper::close() { + std::lock_guard lock(mLock); + if (mHintSession != nullptr) { + gAPH_closeSessionFn(mHintSession); + mHintSession = nullptr; + } +} + +void AdpfWrapper::onBeginCallback() { + if (isOpen()) { + mBeginCallbackNanos = oboe::AudioClock::getNanoseconds(); + } +} + +void AdpfWrapper::onEndCallback(double durationScaler) { + if (isOpen()) { + int64_t endCallbackNanos = oboe::AudioClock::getNanoseconds(); + int64_t actualDurationNanos = endCallbackNanos - mBeginCallbackNanos; + int64_t scaledDurationNanos = static_cast(actualDurationNanos * durationScaler); + reportActualDuration(scaledDurationNanos); + // When the workload is non-zero, update the conversion factor from workload + // units to nanoseconds duration. + if (mPreviousWorkload > 0) { + mNanosPerWorkloadUnit = ((double) scaledDurationNanos) / mPreviousWorkload; + } + } +} + +void AdpfWrapper::reportWorkload(int32_t appWorkload) { + if (isOpen()) { + // Compare with previous workload. If we think we will need more + // time to render the callback then warn ADPF as soon as possible. + if (appWorkload > mPreviousWorkload && mNanosPerWorkloadUnit > 0.0) { + int64_t predictedDuration = (int64_t) (appWorkload * mNanosPerWorkloadUnit); + reportActualDuration(predictedDuration); + } + mPreviousWorkload = appWorkload; + } +} + +oboe::Result AdpfWrapper::notifyWorkloadIncrease(bool cpu, bool gpu, const char* debugName) { + std::lock_guard lock(mLock); + bool traceEnabled = Trace::getInstance().isEnabled(); + if (traceEnabled) { + Trace::getInstance().beginSection("notifyWorkloadIncrease"); + } + if (gAPH_notifyWorkloadIncreaseFn == nullptr) { + return Result::ErrorUnimplemented; + } + if (mHintSession == nullptr) { + return Result::ErrorClosed; + } + int result = gAPH_notifyWorkloadIncreaseFn(mHintSession, cpu, gpu, debugName); + if (result == 0) { + return Result::OK; + } else if (result == EINVAL) { // no hints were requested + return Result::ErrorInvalidHandle; + } else if (result == EBUSY) { // the hint was rate limited + return Result::ErrorInvalidRate; + } else if (result == EPIPE) { // communication with the system service has failed + return Result::ErrorNoService; + } else if (result == ENOTSUP) { // the hint is not supported + return Result::ErrorUnavailable; + } else { + return Result::ErrorInternal; // Unknown error + } + if (traceEnabled) { + Trace::getInstance().endSection(); + } +} + +oboe::Result AdpfWrapper::notifyWorkloadSpike(bool cpu, bool gpu, const char* debugName) { + std::lock_guard lock(mLock); + bool traceEnabled = Trace::getInstance().isEnabled(); + if (traceEnabled) { + Trace::getInstance().beginSection("notifyWorkloadSpike"); + } + if (gAPH_notifyWorkloadSpikeFn == nullptr) { + return Result::ErrorUnimplemented; + } + if (mHintSession == nullptr) { + return Result::ErrorClosed; + } + int result = gAPH_notifyWorkloadSpikeFn(mHintSession, cpu, gpu, debugName); + if (result == 0) { + return Result::OK; + } else if (result == EINVAL) { // no hints were requested + return Result::ErrorInvalidHandle; + } else if (result == EBUSY) { // the hint was rate limited + return Result::ErrorInvalidRate; + } else if (result == EPIPE) { // communication with the system service has failed + return Result::ErrorNoService; + } else if (result == ENOTSUP) { // the hint is not supported + return Result::ErrorUnavailable; + } else { + return Result::ErrorInternal; // Unknown error + } + if (traceEnabled) { + Trace::getInstance().endSection(); + } +} + +oboe::Result AdpfWrapper::notifyWorkloadReset(bool cpu, bool gpu, const char* debugName) { + std::lock_guard lock(mLock); + bool traceEnabled = Trace::getInstance().isEnabled(); + if (traceEnabled) { + Trace::getInstance().beginSection("notifyWorkloadReset"); + } + if (gAPH_notifyWorkloadResetFn == nullptr) { + return Result::ErrorUnimplemented; + } + if (mHintSession == nullptr) { + return Result::ErrorClosed; + } + int result = gAPH_notifyWorkloadResetFn(mHintSession, cpu, gpu, debugName); + if (result == 0) { + return Result::OK; + } else if (result == EINVAL) { // no hints were requested + return Result::ErrorInvalidHandle; + } else if (result == EBUSY) { // the hint was rate limited + return Result::ErrorInvalidRate; + } else if (result == EPIPE) { // communication with the system service has failed + return Result::ErrorNoService; + } else if (result == ENOTSUP) { // the hint is not supported + return Result::ErrorUnavailable; + } else { + return Result::ErrorInternal; // Unknown error + } + if (traceEnabled) { + Trace::getInstance().endSection(); + } +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_AdpfWrapper_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_AdpfWrapper_android.h new file mode 100644 index 0000000..e7a108a --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_AdpfWrapper_android.h @@ -0,0 +1,173 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef SYNTHMARK_ADPF_WRAPPER_H +#define SYNTHMARK_ADPF_WRAPPER_H + +#include +#include +#include +#include +#include +#include + +#include "oboe_oboe_Definitions_android.h" + +namespace oboe { + + struct APerformanceHintManager; + struct APerformanceHintSession; + + typedef struct APerformanceHintManager APerformanceHintManager; + typedef struct APerformanceHintSession APerformanceHintSession; + + class AdpfWrapper { + public: + /** + * Create an ADPF session that can be used to boost performance. + * @param threadId + * @param targetDurationNanos - nominal period of isochronous task + * @return zero or negative error + */ + int open(pid_t threadId, + int64_t targetDurationNanos); + + bool isOpen() const { + return (mHintSession != nullptr); + } + + void close(); + + /** + * Call this at the beginning of the callback that you are measuring. + */ + void onBeginCallback(); + + /** + * Call this at the end of the callback that you are measuring. + * It is OK to skip this if you have a short callback. + */ + void onEndCallback(double durationScaler); + + /** + * For internal use only! + * This is a hack for communicating with experimental versions of ADPF. + * @param enabled + */ + static void setUseAlternative(bool enabled) { + sUseAlternativeHack = enabled; + } + + /** + * Report the measured duration of a callback. + * This is normally called by onEndCallback(). + * You may want to call this directly in order to give an advance hint of a jump in workload. + * @param actualDurationNanos + */ + void reportActualDuration(int64_t actualDurationNanos); + + void reportWorkload(int32_t appWorkload); + + /** + * Informs the framework of an upcoming increase in the workload of an audio callback + * bound to this session. The user can specify whether the increase is expected to be + * on the CPU, GPU, or both. + * + * Sending hints for both CPU and GPU counts as two separate hints for the purposes of the + * rate limiter. + * + * This was introduced in Android API Level 36 + * + * @param cpu Indicates if the workload increase is expected to affect the CPU. + * @param gpu Indicates if the workload increase is expected to affect the GPU. + * @param debugName A required string used to identify this specific hint during + * tracing. This debug string will only be held for the duration of the + * method, and can be safely discarded after. + * + * @return Result::OK on success. + * Result::ErrorClosed if open was not called. + * Result::ErrorUnimplemented if the API is not supported. + * Result::ErrorInvalidHandle if no hints were requested. + * Result::ErrorInvalidRate if the hint was rate limited. + * Result::ErrorNoService if communication with the system service has failed. + * Result::ErrorUnavailable if the hint is not supported. + */ + oboe::Result notifyWorkloadIncrease(bool cpu, bool gpu, const char* debugName); + + /** + * Informs the framework of an upcoming reset in the workload of an audio callback + * bound to this session, or the imminent start of a new workload. The user can specify + * whether the reset is expected to affect the CPU, GPU, or both. + * + * Sending hints for both CPU and GPU counts as two separate hints for the purposes of the + * this load tracking. + * + * This was introduced in Android API Level 36 + * + * @param cpu Indicates if the workload reset is expected to affect the CPU. + * @param gpu Indicates if the workload reset is expected to affect the GPU. + * @param debugName A required string used to identify this specific hint during + * tracing. This debug string will only be held for the duration of the + * method, and can be safely discarded after. + * + * @return Result::OK on success. + * Result::ErrorClosed if open was not called. + * Result::ErrorUnimplemented if the API is not supported. + * Result::ErrorInvalidHandle if no hints were requested. + * Result::ErrorInvalidRate if the hint was rate limited. + * Result::ErrorNoService if communication with the system service has failed. + * Result::ErrorUnavailable if the hint is not supported. + */ + oboe::Result notifyWorkloadReset(bool cpu, bool gpu, const char* debugName); + + /** + * Informs the framework of an upcoming one-off expensive frame for an audio callback + * bound to this session. This frame will be treated as not representative of the workload as a + * whole, and it will be discarded the purposes of load tracking. The user can specify + * whether the workload spike is expected to be on the CPU, GPU, or both. + * + * Sending hints for both CPU and GPU counts as two separate hints for the purposes of the + * rate limiter. + * + * This was introduced in Android API Level 36 + * + * @param cpu Indicates if the workload spike is expected to affect the CPU. + * @param gpu Indicates if the workload spike is expected to affect the GPU. + * @param debugName A required string used to identify this specific hint during + * tracing. This debug string will only be held for the duration of the + * method, and can be safely discarded after. + * + * @return Result::OK on success. + * Result::ErrorClosed if open was not called. + * Result::ErrorUnimplemented if the API is not supported. + * Result::ErrorInvalidHandle if no hints were requested. + * Result::ErrorInvalidRate if the hint was rate limited. + * Result::ErrorNoService if communication with the system service has failed. + * Result::ErrorUnavailable if the hint is not supported. + */ + oboe::Result notifyWorkloadSpike(bool cpu, bool gpu, const char* debugName); + + private: + std::mutex mLock; + APerformanceHintSession *mHintSession = nullptr; + int64_t mBeginCallbackNanos = 0; + static bool sUseAlternativeHack; + int32_t mPreviousWorkload = 0; + double mNanosPerWorkloadUnit = 0.0; + }; + +} +#endif //SYNTHMARK_ADPF_WRAPPER_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_AudioSourceCaller_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_AudioSourceCaller_android.cpp new file mode 100644 index 0000000..d1a9ac1 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_AudioSourceCaller_android.cpp @@ -0,0 +1,38 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "oboe_common_AudioSourceCaller_android.h" + +using namespace oboe; +using namespace flowgraph; + +int32_t AudioSourceCaller::onProcessFixedBlock(uint8_t *buffer, int32_t numBytes) { + AudioStreamDataCallback *callback = mStream->getDataCallback(); + int32_t result = 0; + int32_t numFrames = numBytes / mStream->getBytesPerFrame(); + if (callback != nullptr) { + DataCallbackResult callbackResult = callback->onAudioReady(mStream, buffer, numFrames); + // onAudioReady() does not return the number of bytes processed so we have to assume all. + result = (callbackResult == DataCallbackResult::Continue) + ? numBytes + : -1; + } else { + auto readResult = mStream->read(buffer, numFrames, mTimeoutNanos); + if (!readResult) return (int32_t) readResult.error(); + result = readResult.value() * mStream->getBytesPerFrame(); + } + return result; +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_AudioSourceCaller_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_AudioSourceCaller_android.h new file mode 100644 index 0000000..5ac784e --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_AudioSourceCaller_android.h @@ -0,0 +1,83 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_AUDIO_SOURCE_CALLER_H +#define OBOE_AUDIO_SOURCE_CALLER_H + +#include "oboe_common_OboeDebug_android.h" +#include "oboe_oboe_Oboe_android.h" + +#include "oboe_flowgraph_FlowGraphNode_android.h" +#include "oboe_common_FixedBlockReader_android.h" + +namespace oboe { + +class AudioStreamCallback; +class AudioStream; + +/** + * For output streams that use a callback, call the application for more data. + * For input streams that do not use a callback, read from the stream. + */ +class AudioSourceCaller : public flowgraph::FlowGraphSource, public FixedBlockProcessor { +public: + AudioSourceCaller(int32_t channelCount, int32_t framesPerCallback, int32_t bytesPerSample) + : FlowGraphSource(channelCount) + , mBlockReader(*this) { + mBlockReader.open(channelCount * framesPerCallback * bytesPerSample); + } + + /** + * Set the stream to use as a source of data. + * @param stream + */ + void setStream(oboe::AudioStream *stream) { + mStream = stream; + } + + oboe::AudioStream *getStream() { + return mStream; + } + + /** + * Timeout value to use when calling audioStream->read(). + * @param timeoutNanos Zero for no timeout or time in nanoseconds. + */ + void setTimeoutNanos(int64_t timeoutNanos) { + mTimeoutNanos = timeoutNanos; + } + + int64_t getTimeoutNanos() const { + return mTimeoutNanos; + } + + /** + * Called internally for block size adaptation. + * @param buffer + * @param numBytes + * @return + */ + int32_t onProcessFixedBlock(uint8_t *buffer, int32_t numBytes) override; + +protected: + oboe::AudioStream *mStream = nullptr; + int64_t mTimeoutNanos = 0; + + FixedBlockReader mBlockReader; +}; + +} +#endif //OBOE_AUDIO_SOURCE_CALLER_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_AudioStreamBuilder_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_AudioStreamBuilder_android.cpp new file mode 100644 index 0000000..fe5b840 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_AudioStreamBuilder_android.cpp @@ -0,0 +1,252 @@ +/* + * Copyright 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + + +#include "oboe_aaudio_AAudioExtensions_android.h" +#include "oboe_aaudio_AudioStreamAAudio_android.h" +#include "oboe_common_OboeDebug_android.h" +#include "oboe_oboe_Oboe_android.h" +#include "oboe_oboe_AudioStreamBuilder_android.h" +#include "oboe_opensles_AudioInputStreamOpenSLES_android.h" +#include "oboe_opensles_AudioOutputStreamOpenSLES_android.h" +#include "oboe_opensles_AudioStreamOpenSLES_android.h" +#include "oboe_common_QuirksManager_android.h" + +#ifndef DISABLE_CONVERSION +#include "oboe_common_FilterAudioStream_android.h" +#endif + +bool oboe::OboeGlobals::mWorkaroundsEnabled = true; + +namespace oboe { + +/** + * The following default values are used when oboe does not have any better way of determining the optimal values + * for an audio stream. This can happen when: + * + * - Client is creating a stream on API < 26 (OpenSLES) but has not supplied the optimal sample + * rate and/or frames per burst + * - Client is creating a stream on API 16 (OpenSLES) where AudioManager.PROPERTY_OUTPUT_* values + * are not available + */ +int32_t DefaultStreamValues::SampleRate = 48000; // Common rate for mobile audio and video +int32_t DefaultStreamValues::FramesPerBurst = 192; // 4 msec at 48000 Hz +int32_t DefaultStreamValues::ChannelCount = 2; // Stereo + +constexpr int knumBurstsForLowLatencyStreams = 2; + +#ifndef OBOE_ENABLE_AAUDIO +// Set OBOE_ENABLE_AAUDIO to 0 if you want to disable the AAudio API. +// This might be useful if you want to force all the unit tests to use OpenSL ES. +#define OBOE_ENABLE_AAUDIO 1 +#endif + +bool AudioStreamBuilder::isAAudioSupported() { + return AudioStreamAAudio::isSupported() && OBOE_ENABLE_AAUDIO; +} + +bool AudioStreamBuilder::isAAudioRecommended() { + // See https://github.com/google/oboe/issues/40, + // AAudio may not be stable on Android O, depending on how it is used. + // To be safe, use AAudio only on O_MR1 and above. + return (getSdkVersion() >= __ANDROID_API_O_MR1__) && isAAudioSupported(); +} + +AudioStream *AudioStreamBuilder::build() { + AudioStream *stream = nullptr; + if (isAAudioRecommended() && mAudioApi != AudioApi::OpenSLES) { + stream = new AudioStreamAAudio(*this); + } else if (isAAudioSupported() && mAudioApi == AudioApi::AAudio) { + stream = new AudioStreamAAudio(*this); + LOGE("Creating AAudio stream on 8.0 because it was specified. This is error prone."); + } else { + if (getDirection() == oboe::Direction::Output) { + stream = new AudioOutputStreamOpenSLES(*this); + } else if (getDirection() == oboe::Direction::Input) { + stream = new AudioInputStreamOpenSLES(*this); + } + } + return stream; +} + +bool AudioStreamBuilder::isCompatible(AudioStreamBase &other) { + return (getSampleRate() == oboe::Unspecified || getSampleRate() == other.getSampleRate()) + && (getFormat() == (AudioFormat)oboe::Unspecified || getFormat() == other.getFormat()) + && (getFramesPerDataCallback() == oboe::Unspecified || getFramesPerDataCallback() == other.getFramesPerDataCallback()) + && (getChannelCount() == oboe::Unspecified || getChannelCount() == other.getChannelCount()); +} + +Result AudioStreamBuilder::openStream(AudioStream **streamPP) { + LOGW("Passing AudioStream pointer deprecated, Use openStream(std::shared_ptr &stream) instead."); + return openStreamInternal(streamPP); +} + +Result AudioStreamBuilder::openStreamInternal(AudioStream **streamPP) { + auto result = isValidConfig(); + if (result != Result::OK) { + LOGW("%s() invalid config. Error %s", __func__, oboe::convertToText(result)); + return result; + } + if (mPartialDataCallback != nullptr && + (!OboeExtensions::isPartialDataCallbackSupported() || !willUseAAudio())) { + LOGE("%s() Partial data callback is not supported.", __func__); + return Result::ErrorIllegalArgument; + } + +#ifndef OBOE_SUPPRESS_LOG_SPAM + LOGI("%s() %s -------- %s --------", + __func__, getDirection() == Direction::Input ? "INPUT" : "OUTPUT", getVersionText()); +#endif + + if (streamPP == nullptr) { + return Result::ErrorNull; + } + *streamPP = nullptr; + + AudioStream *streamP = nullptr; + + // Maybe make a FilterInputStream. + AudioStreamBuilder childBuilder(*this); + +#ifndef DISABLE_CONVERSION + // Check need for conversion and modify childBuilder for optimal stream. + bool conversionNeeded = QuirksManager::getInstance().isConversionNeeded(*this, childBuilder); + // Do we need to make a child stream and convert. + if (conversionNeeded) { + if (isPartialDataCallbackSpecified()) { + LOGW("%s(), partial data callback is not supported when data conversion is required", + __func__); + return Result::ErrorIllegalArgument; + } + AudioStream *tempStream; + result = childBuilder.openStreamInternal(&tempStream); + if (result != Result::OK) { + return result; + } + + if (isCompatible(*tempStream)) { + // The child stream would work as the requested stream so we can just use it directly. + *streamPP = tempStream; + return result; + } else { + AudioStreamBuilder parentBuilder = *this; + // Build a stream that is as close as possible to the childStream. + if (getFormat() == oboe::AudioFormat::Unspecified) { + parentBuilder.setFormat(tempStream->getFormat()); + } + if (getChannelCount() == oboe::Unspecified) { + parentBuilder.setChannelCount(tempStream->getChannelCount()); + } + if (getSampleRate() == oboe::Unspecified) { + parentBuilder.setSampleRate(tempStream->getSampleRate()); + } + if (getFramesPerDataCallback() == oboe::Unspecified) { + parentBuilder.setFramesPerCallback(tempStream->getFramesPerDataCallback()); + } + + // Use childStream in a FilterAudioStream. + LOGI("%s() create a FilterAudioStream for data conversion.", __func__); + std::shared_ptr childStream(tempStream); + FilterAudioStream *filterStream = new FilterAudioStream(parentBuilder, childStream); + childStream->setWeakThis(childStream); + result = filterStream->configureFlowGraph(); + if (result != Result::OK) { + filterStream->close(); + delete filterStream; + // Just open streamP the old way. + } else { + streamP = static_cast(filterStream); + } + } + } +#endif + + if (streamP == nullptr) { + streamP = build(); + if (streamP == nullptr) { + return Result::ErrorNull; + } + } + + // If MMAP has a problem in this case then disable it temporarily. + bool wasMMapOriginallyEnabled = AAudioExtensions::getInstance().isMMapEnabled(); + bool wasMMapTemporarilyDisabled = false; + if (wasMMapOriginallyEnabled) { + bool isMMapSafe = QuirksManager::getInstance().isMMapSafe(childBuilder); + if (!isMMapSafe) { + AAudioExtensions::getInstance().setMMapEnabled(false); + wasMMapTemporarilyDisabled = true; + } + } + result = streamP->open(); + if (wasMMapTemporarilyDisabled) { + AAudioExtensions::getInstance().setMMapEnabled(wasMMapOriginallyEnabled); // restore original + } + if (result == Result::OK) { + // AAudio supports setBufferSizeInFrames() so use it. + if (streamP->getAudioApi() == AudioApi::AAudio) { + int32_t optimalBufferSize = -1; + // Use a reasonable default buffer size. + if (streamP->getDirection() == Direction::Input) { + // For input, small size does not improve latency because the stream is usually + // run close to empty. And a low size can result in XRuns so always use the maximum. + optimalBufferSize = streamP->getBufferCapacityInFrames(); + } else if (streamP->getPerformanceMode() == PerformanceMode::LowLatency + && streamP->getDirection() == Direction::Output) { // Output check is redundant. + optimalBufferSize = streamP->getFramesPerBurst() * + knumBurstsForLowLatencyStreams; + } + if (optimalBufferSize >= 0) { + auto setBufferResult = streamP->setBufferSizeInFrames(optimalBufferSize); + if (!setBufferResult) { + LOGW("Failed to setBufferSizeInFrames(%d). Error was %s", + optimalBufferSize, + convertToText(setBufferResult.error())); + } + } + } + + *streamPP = streamP; + } else { + delete streamP; + } + return result; +} + +Result AudioStreamBuilder::openManagedStream(oboe::ManagedStream &stream) { + LOGW("`openManagedStream` is deprecated. Use openStream(std::shared_ptr &stream) instead."); + stream.reset(); + AudioStream *streamptr; + auto result = openStream(&streamptr); + stream.reset(streamptr); + return result; +} + +Result AudioStreamBuilder::openStream(std::shared_ptr &sharedStream) { + sharedStream.reset(); + AudioStream *streamptr; + auto result = openStreamInternal(&streamptr); + if (result == Result::OK) { + sharedStream.reset(streamptr); + // Save a weak_ptr in the stream for use with callbacks. + streamptr->setWeakThis(sharedStream); + } + return result; +} + +} // namespace oboe diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_AudioStream_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_AudioStream_android.cpp new file mode 100644 index 0000000..80160ae --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_AudioStream_android.cpp @@ -0,0 +1,258 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include "oboe_oboe_AudioClock_android.h" +#include "oboe_oboe_AudioStream_android.h" + +#include + +#include "oboe_oboe_Utilities_android.h" +#include "oboe_common_OboeDebug_android.h" + +namespace oboe { + +/* + * AudioStream + */ +AudioStream::AudioStream(const AudioStreamBuilder &builder) + : AudioStreamBase(builder) { + LOGD("Constructor for AudioStream at %p", this); +} + +AudioStream::~AudioStream() { + // This is to help debug use after free bugs. + LOGD("Destructor for AudioStream at %p", this); +} + +Result AudioStream::close() { + closePerformanceHint(); + // Update local counters so they can be read after the close. + updateFramesWritten(); + updateFramesRead(); + return Result::OK; +} + +// Call this from fireDataCallback() if you want to monitor CPU scheduler. +void AudioStream::checkScheduler() { + int scheduler = sched_getscheduler(0) & ~SCHED_RESET_ON_FORK; // for current thread + if (scheduler != mPreviousScheduler) { + LOGD("AudioStream::%s() scheduler = %s", __func__, + ((scheduler == SCHED_FIFO) ? "SCHED_FIFO" : + ((scheduler == SCHED_OTHER) ? "SCHED_OTHER" : + ((scheduler == SCHED_RR) ? "SCHED_RR" : "UNKNOWN"))) + ); + mPreviousScheduler = scheduler; + } +} + +DataCallbackResult AudioStream::fireDataCallback(void *audioData, int32_t numFrames) { + if (!isDataCallbackEnabled()) { + LOGW("AudioStream::%s() called with data callback disabled!", __func__); + return DataCallbackResult::Stop; // Should not be getting called + } + + beginPerformanceHintInCallback(); + + // Call the app to do the work. + DataCallbackResult result; + if (mDataCallback) { + result = mDataCallback->onAudioReady(this, audioData, numFrames); + } else { + result = onDefaultCallback(audioData, numFrames); + } + // On Oreo, we might get called after returning stop. + // So block that here. + setDataCallbackEnabled(result == DataCallbackResult::Continue); + + endPerformanceHintInCallback(numFrames); + + return result; +} + +int32_t AudioStream::firePartialDataCallback(void *audioData, int numFrames) { + if (!isDataCallbackEnabled()) { + LOGW("AudioStream::%s() called with data callback disabled!", __func__); + return -1; // Should not be getting called + } + + beginPerformanceHintInCallback(); + + // Call the app to do the work. + int32_t result; + if (mPartialDataCallback) { + result = mPartialDataCallback->onPartialAudioReady(this, audioData, numFrames); + } else { + LOGE("AudioStream::%s, called without a partial data callback!", __func__); + result = -1; // This should not happen, return negative value to stop the stream. + } + + endPerformanceHintInCallback(numFrames); + + return result; +} + +Result AudioStream::waitForStateTransition(StreamState startingState, + StreamState endingState, + int64_t timeoutNanoseconds) +{ + StreamState state; + { + std::lock_guard lock(mLock); + state = getState(); + if (state == StreamState::Closed) { + return Result::ErrorClosed; + } else if (state == StreamState::Disconnected) { + return Result::ErrorDisconnected; + } + } + + StreamState nextState = state; + // TODO Should this be a while()?! + if (state == startingState && state != endingState) { + Result result = waitForStateChange(state, &nextState, timeoutNanoseconds); + if (result != Result::OK) { + return result; + } + } + + if (nextState != endingState) { + return Result::ErrorInvalidState; + } else { + return Result::OK; + } +} + +Result AudioStream::start(int64_t timeoutNanoseconds) +{ + Result result = requestStart(); + if (result != Result::OK) return result; + if (timeoutNanoseconds <= 0) return result; + result = waitForStateTransition(StreamState::Starting, + StreamState::Started, timeoutNanoseconds); + if (result != Result::OK) { + LOGE("AudioStream::%s() timed out before moving from STARTING to STARTED", __func__); + } + return result; +} + +Result AudioStream::pause(int64_t timeoutNanoseconds) +{ + Result result = requestPause(); + if (result != Result::OK) return result; + if (timeoutNanoseconds <= 0) return result; + return waitForStateTransition(StreamState::Pausing, + StreamState::Paused, timeoutNanoseconds); +} + +Result AudioStream::flush(int64_t timeoutNanoseconds) +{ + Result result = requestFlush(); + if (result != Result::OK) return result; + if (timeoutNanoseconds <= 0) return result; + return waitForStateTransition(StreamState::Flushing, + StreamState::Flushed, timeoutNanoseconds); +} + +Result AudioStream::stop(int64_t timeoutNanoseconds) +{ + Result result = requestStop(); + if (result != Result::OK) return result; + if (timeoutNanoseconds <= 0) return result; + return waitForStateTransition(StreamState::Stopping, + StreamState::Stopped, timeoutNanoseconds); +} + +int32_t AudioStream::getBytesPerSample() const { + return convertFormatToSizeInBytes(mFormat); +} + +int64_t AudioStream::getFramesRead() { + updateFramesRead(); + return mFramesRead; +} + +int64_t AudioStream::getFramesWritten() { + updateFramesWritten(); + return mFramesWritten; +} + +ResultWithValue AudioStream::getAvailableFrames() { + int64_t readCounter = getFramesRead(); + if (readCounter < 0) return ResultWithValue::createBasedOnSign(readCounter); + int64_t writeCounter = getFramesWritten(); + if (writeCounter < 0) return ResultWithValue::createBasedOnSign(writeCounter); + int32_t framesAvailable = writeCounter - readCounter; + return ResultWithValue(framesAvailable); +} + +ResultWithValue AudioStream::waitForAvailableFrames(int32_t numFrames, + int64_t timeoutNanoseconds) { + if (numFrames == 0) return Result::OK; + if (numFrames < 0) return Result::ErrorOutOfRange; + + // Make sure we don't try to wait for more frames than the buffer can hold. + // Subtract framesPerBurst because this is often called from a callback + // and we don't want to be sleeping if the buffer is close to overflowing. + const int32_t maxAvailableFrames = getBufferCapacityInFrames() - getFramesPerBurst(); + numFrames = std::min(numFrames, maxAvailableFrames); + // The capacity should never be less than one burst. But clip to zero just in case. + numFrames = std::max(0, numFrames); + + int64_t framesAvailable = 0; + int64_t burstInNanos = getFramesPerBurst() * kNanosPerSecond / getSampleRate(); + bool ready = false; + int64_t deadline = AudioClock::getNanoseconds() + timeoutNanoseconds; + do { + ResultWithValue result = getAvailableFrames(); + if (!result) return result; + framesAvailable = result.value(); + ready = (framesAvailable >= numFrames); + if (!ready) { + int64_t now = AudioClock::getNanoseconds(); + if (now > deadline) break; + AudioClock::sleepForNanos(burstInNanos); + } + } while (!ready); + return (!ready) + ? ResultWithValue(Result::ErrorTimeout) + : ResultWithValue(framesAvailable); +} + +ResultWithValue AudioStream::getTimestamp(clockid_t clockId) { + FrameTimestamp frame; + Result result = getTimestamp(clockId, &frame.position, &frame.timestamp); + if (result == Result::OK){ + return ResultWithValue(frame); + } else { + return ResultWithValue(static_cast(result)); + } +} + +void AudioStream::calculateDefaultDelayBeforeCloseMillis() { + // Calculate delay time before close based on burst duration. + // Start with a burst duration then add 1 msec as a safety margin. + mDelayBeforeCloseMillis = std::clamp(1 + (mFramesPerBurst * 1000) / getSampleRate(), + kMinDelayBeforeCloseMillis, + kMaxDelayBeforeCloseMillis); + LOGD("calculateDefaultDelayBeforeCloseMillis() default = %d", + static_cast(mDelayBeforeCloseMillis)); +} + +} // namespace oboe diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_DataConversionFlowGraph_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_DataConversionFlowGraph_android.cpp new file mode 100644 index 0000000..65a5306 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_DataConversionFlowGraph_android.cpp @@ -0,0 +1,266 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "oboe_common_OboeDebug_android.h" +#include "oboe_common_DataConversionFlowGraph_android.h" +#include "oboe_common_SourceFloatCaller_android.h" +#include "oboe_common_SourceI16Caller_android.h" +#include "oboe_common_SourceI24Caller_android.h" +#include "oboe_common_SourceI32Caller_android.h" + +#include "oboe_flowgraph_MonoToMultiConverter_android.h" +#include "oboe_flowgraph_MultiToMonoConverter_android.h" +#include "oboe_flowgraph_RampLinear_android.h" +#include "oboe_flowgraph_SinkFloat_android.h" +#include "oboe_flowgraph_SinkI16_android.h" +#include "oboe_flowgraph_SinkI24_android.h" +#include "oboe_flowgraph_SinkI32_android.h" +#include "oboe_flowgraph_SourceFloat_android.h" +#include "oboe_flowgraph_SourceI16_android.h" +#include "oboe_flowgraph_SourceI24_android.h" +#include "oboe_flowgraph_SourceI32_android.h" +#include "oboe_flowgraph_SampleRateConverter_android.h" + +using namespace oboe; +using namespace flowgraph; +using namespace resampler; + +void DataConversionFlowGraph::setSource(const void *buffer, int32_t numFrames) { + mSource->setData(buffer, numFrames); +} + +static MultiChannelResampler::Quality convertOboeSRQualityToMCR(SampleRateConversionQuality quality) { + switch (quality) { + case SampleRateConversionQuality::Fastest: + return MultiChannelResampler::Quality::Fastest; + case SampleRateConversionQuality::Low: + return MultiChannelResampler::Quality::Low; + default: + case SampleRateConversionQuality::Medium: + return MultiChannelResampler::Quality::Medium; + case SampleRateConversionQuality::High: + return MultiChannelResampler::Quality::High; + case SampleRateConversionQuality::Best: + return MultiChannelResampler::Quality::Best; + } +} + +// Chain together multiple processors. +// Callback Output +// Use SourceCaller that calls original app callback from the flowgraph. +// The child callback from FilteredAudioStream read()s from the flowgraph. +// Callback Input +// Child callback from FilteredAudioStream writes()s to the flowgraph. +// The output of the flowgraph goes through a BlockWriter to the app callback. +// Blocking Write +// Write buffer is set on an AudioSource. +// Data is pulled through the graph and written to the child stream. +// Blocking Read +// Reads in a loop from the flowgraph Sink to fill the read buffer. +// A SourceCaller then does a blocking read from the child Stream. +// +Result DataConversionFlowGraph::configure(AudioStream *sourceStream, AudioStream *sinkStream) { + + FlowGraphPortFloatOutput *lastOutput = nullptr; + + bool isOutput = sourceStream->getDirection() == Direction::Output; + bool isInput = !isOutput; + mFilterStream = isOutput ? sourceStream : sinkStream; + + AudioFormat sourceFormat = sourceStream->getFormat(); + int32_t sourceChannelCount = sourceStream->getChannelCount(); + int32_t sourceSampleRate = sourceStream->getSampleRate(); + int32_t sourceFramesPerCallback = sourceStream->getFramesPerDataCallback(); + + AudioFormat sinkFormat = sinkStream->getFormat(); + int32_t sinkChannelCount = sinkStream->getChannelCount(); + int32_t sinkSampleRate = sinkStream->getSampleRate(); + int32_t sinkFramesPerCallback = sinkStream->getFramesPerDataCallback(); + + LOGI("%s() flowgraph converts channels: %d to %d, format: %s to %s" + ", rate: %d to %d, cbsize: %d to %d, qual = %s", + __func__, + sourceChannelCount, sinkChannelCount, + oboe::convertToText(sourceFormat), oboe::convertToText(sinkFormat), + sourceSampleRate, sinkSampleRate, + sourceFramesPerCallback, sinkFramesPerCallback, + oboe::convertToText(sourceStream->getSampleRateConversionQuality())); + + // Source + // IF OUTPUT and using a callback then call back to the app using a SourceCaller. + // OR IF INPUT and NOT using a callback then read from the child stream using a SourceCaller. + bool isDataCallbackSpecified = sourceStream->isDataCallbackSpecified(); + if ((isDataCallbackSpecified && isOutput) + || (!isDataCallbackSpecified && isInput)) { + int32_t actualSourceFramesPerCallback = (sourceFramesPerCallback == kUnspecified) + ? sourceStream->getFramesPerBurst() + : sourceFramesPerCallback; + switch (sourceFormat) { + case AudioFormat::Float: + mSourceCaller = std::make_unique(sourceChannelCount, + actualSourceFramesPerCallback); + break; + case AudioFormat::I16: + mSourceCaller = std::make_unique(sourceChannelCount, + actualSourceFramesPerCallback); + break; + case AudioFormat::I24: + mSourceCaller = std::make_unique(sourceChannelCount, + actualSourceFramesPerCallback); + break; + case AudioFormat::I32: + mSourceCaller = std::make_unique(sourceChannelCount, + actualSourceFramesPerCallback); + break; + default: + LOGE("%s() Unsupported source caller format = %d", __func__, static_cast(sourceFormat)); + return Result::ErrorIllegalArgument; + } + mSourceCaller->setStream(sourceStream); + lastOutput = &mSourceCaller->output; + } else { + // IF OUTPUT and NOT using a callback then write to the child stream using a BlockWriter. + // OR IF INPUT and using a callback then write to the app using a BlockWriter. + switch (sourceFormat) { + case AudioFormat::Float: + mSource = std::make_unique(sourceChannelCount); + break; + case AudioFormat::I16: + mSource = std::make_unique(sourceChannelCount); + break; + case AudioFormat::I24: + mSource = std::make_unique(sourceChannelCount); + break; + case AudioFormat::I32: + mSource = std::make_unique(sourceChannelCount); + break; + default: + LOGE("%s() Unsupported source format = %d", __func__, static_cast(sourceFormat)); + return Result::ErrorIllegalArgument; + } + if (isInput) { + int32_t actualSinkFramesPerCallback = (sinkFramesPerCallback == kUnspecified) + ? sinkStream->getFramesPerBurst() + : sinkFramesPerCallback; + // The BlockWriter is after the Sink so use the SinkStream size. + mBlockWriter.open(actualSinkFramesPerCallback * sinkStream->getBytesPerFrame()); + mAppBuffer = std::make_unique( + kDefaultBufferSize * sinkStream->getBytesPerFrame()); + } + lastOutput = &mSource->output; + } + + // If we are going to reduce the number of channels then do it before the + // sample rate converter. + if (sourceChannelCount > sinkChannelCount) { + if (sinkChannelCount == 1) { + mMultiToMonoConverter = std::make_unique(sourceChannelCount); + lastOutput->connect(&mMultiToMonoConverter->input); + lastOutput = &mMultiToMonoConverter->output; + } else { + mChannelCountConverter = std::make_unique( + sourceChannelCount, + sinkChannelCount); + lastOutput->connect(&mChannelCountConverter->input); + lastOutput = &mChannelCountConverter->output; + } + } + + // Sample Rate conversion + if (sourceSampleRate != sinkSampleRate) { + // Create a resampler to do the math. + mResampler.reset(MultiChannelResampler::make(lastOutput->getSamplesPerFrame(), + sourceSampleRate, + sinkSampleRate, + convertOboeSRQualityToMCR( + sourceStream->getSampleRateConversionQuality()))); + // Make a flowgraph node that uses the resampler. + mRateConverter = std::make_unique(lastOutput->getSamplesPerFrame(), + *mResampler.get()); + lastOutput->connect(&mRateConverter->input); + lastOutput = &mRateConverter->output; + } + + // Expand the number of channels if required. + if (sourceChannelCount < sinkChannelCount) { + if (sourceChannelCount == 1) { + mMonoToMultiConverter = std::make_unique(sinkChannelCount); + lastOutput->connect(&mMonoToMultiConverter->input); + lastOutput = &mMonoToMultiConverter->output; + } else { + mChannelCountConverter = std::make_unique( + sourceChannelCount, + sinkChannelCount); + lastOutput->connect(&mChannelCountConverter->input); + lastOutput = &mChannelCountConverter->output; + } + } + + // Sink + switch (sinkFormat) { + case AudioFormat::Float: + mSink = std::make_unique(sinkChannelCount); + break; + case AudioFormat::I16: + mSink = std::make_unique(sinkChannelCount); + break; + case AudioFormat::I24: + mSink = std::make_unique(sinkChannelCount); + break; + case AudioFormat::I32: + mSink = std::make_unique(sinkChannelCount); + break; + default: + LOGE("%s() Unsupported sink format = %d", __func__, static_cast(sinkFormat)); + return Result::ErrorIllegalArgument;; + } + lastOutput->connect(&mSink->input); + + return Result::OK; +} + +int32_t DataConversionFlowGraph::read(void *buffer, int32_t numFrames, int64_t timeoutNanos) { + if (mSourceCaller) { + mSourceCaller->setTimeoutNanos(timeoutNanos); + } + int32_t numRead = mSink->read(buffer, numFrames); + return numRead; +} + +// This is similar to pushing data through the flowgraph. +int32_t DataConversionFlowGraph::write(void *inputBuffer, int32_t numFrames) { + // Put the data from the input at the head of the flowgraph. + mSource->setData(inputBuffer, numFrames); + while (true) { + // Pull and read some data in app format into a small buffer. + int32_t framesRead = mSink->read(mAppBuffer.get(), flowgraph::kDefaultBufferSize); + if (framesRead <= 0) break; + // Write to a block adapter, which will call the destination whenever it has enough data. + int32_t bytesRead = mBlockWriter.write(mAppBuffer.get(), + framesRead * mFilterStream->getBytesPerFrame()); + if (bytesRead < 0) return bytesRead; // TODO review + } + return numFrames; +} + +int32_t DataConversionFlowGraph::onProcessFixedBlock(uint8_t *buffer, int32_t numBytes) { + int32_t numFrames = numBytes / mFilterStream->getBytesPerFrame(); + mCallbackResult = mFilterStream->getDataCallback()->onAudioReady(mFilterStream, buffer, numFrames); + // TODO handle STOP from callback, process data remaining in the block adapter + return numBytes; +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_DataConversionFlowGraph_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_DataConversionFlowGraph_android.h new file mode 100644 index 0000000..53baee9 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_DataConversionFlowGraph_android.h @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_OBOE_FLOW_GRAPH_H +#define OBOE_OBOE_FLOW_GRAPH_H + +#include +#include +#include + +#include "oboe_flowgraph_ChannelCountConverter_android.h" +#include "oboe_flowgraph_MonoToMultiConverter_android.h" +#include "oboe_flowgraph_MultiToMonoConverter_android.h" +#include "oboe_flowgraph_SampleRateConverter_android.h" +#include "oboe_oboe_Definitions_android.h" +#include "oboe_common_AudioSourceCaller_android.h" +#include "oboe_common_FixedBlockWriter_android.h" + +namespace oboe { + +class AudioStream; +class AudioSourceCaller; + +/** + * Convert PCM channels, format and sample rate for optimal latency. + */ +class DataConversionFlowGraph : public FixedBlockProcessor { +public: + + DataConversionFlowGraph() + : mBlockWriter(*this) {} + + void setSource(const void *buffer, int32_t numFrames); + + /** Connect several modules together to convert from source to sink. + * This should only be called once for each instance. + * + * @param sourceFormat + * @param sourceChannelCount + * @param sinkFormat + * @param sinkChannelCount + * @return + */ + oboe::Result configure(oboe::AudioStream *sourceStream, oboe::AudioStream *sinkStream); + + int32_t read(void *buffer, int32_t numFrames, int64_t timeoutNanos); + + int32_t write(void *buffer, int32_t numFrames); + + int32_t onProcessFixedBlock(uint8_t *buffer, int32_t numBytes) override; + + DataCallbackResult getDataCallbackResult() { + return mCallbackResult; + } + +private: + std::unique_ptr mSource; + std::unique_ptr mSourceCaller; + std::unique_ptr mMonoToMultiConverter; + std::unique_ptr mMultiToMonoConverter; + std::unique_ptr mChannelCountConverter; + std::unique_ptr mResampler; + std::unique_ptr mRateConverter; + std::unique_ptr mSink; + + FixedBlockWriter mBlockWriter; + DataCallbackResult mCallbackResult = DataCallbackResult::Continue; + AudioStream *mFilterStream = nullptr; + std::unique_ptr mAppBuffer; +}; + +} +#endif //OBOE_OBOE_FLOW_GRAPH_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_FilterAudioStream_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_FilterAudioStream_android.cpp new file mode 100644 index 0000000..768263f --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_FilterAudioStream_android.cpp @@ -0,0 +1,106 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "oboe_common_OboeDebug_android.h" +#include "oboe_common_FilterAudioStream_android.h" + +using namespace oboe; +using namespace flowgraph; + +// Output callback uses FixedBlockReader::read() +// <= SourceFloatCaller::onProcess() +// <=== DataConversionFlowGraph::read() +// <== FilterAudioStream::onAudioReady() +// +// Output blocking uses no block adapter because AAudio can accept +// writes of any size. It uses DataConversionFlowGraph::read() <== FilterAudioStream::write() <= app +// +// Input callback uses FixedBlockWriter::write() +// <= DataConversionFlowGraph::write() +// <= FilterAudioStream::onAudioReady() +// +// Input blocking uses FixedBlockReader::read() // TODO may not need block adapter +// <= SourceFloatCaller::onProcess() +// <=== SinkFloat::read() +// <= DataConversionFlowGraph::read() +// <== FilterAudioStream::read() +// <= app + +Result FilterAudioStream::configureFlowGraph() { + mFlowGraph = std::make_unique(); + bool isOutput = getDirection() == Direction::Output; + + AudioStream *sourceStream = isOutput ? this : mChildStream.get(); + AudioStream *sinkStream = isOutput ? mChildStream.get() : this; + + mRateScaler = ((double) getSampleRate()) / mChildStream->getSampleRate(); + + return mFlowGraph->configure(sourceStream, sinkStream); +} + +// Put the data to be written at the source end of the flowgraph. +// Then read (pull) the data from the flowgraph and write it to the +// child stream. +ResultWithValue FilterAudioStream::write(const void *buffer, + int32_t numFrames, + int64_t timeoutNanoseconds) { + int32_t framesWritten = 0; + mFlowGraph->setSource(buffer, numFrames); + while (true) { + int32_t numRead = mFlowGraph->read(mBlockingBuffer.get(), + getFramesPerBurst(), + timeoutNanoseconds); + if (numRead < 0) { + return ResultWithValue::createBasedOnSign(numRead); + } + if (numRead == 0) { + break; // finished processing the source buffer + } + auto writeResult = mChildStream->write(mBlockingBuffer.get(), + numRead, + timeoutNanoseconds); + if (!writeResult) { + return writeResult; + } + framesWritten += writeResult.value(); + } + return ResultWithValue::createBasedOnSign(framesWritten); +} + +// Read (pull) the data we want from the sink end of the flowgraph. +// The necessary data will be read from the child stream using a flowgraph callback. +ResultWithValue FilterAudioStream::read(void *buffer, + int32_t numFrames, + int64_t timeoutNanoseconds) { + int32_t framesRead = mFlowGraph->read(buffer, numFrames, timeoutNanoseconds); + return ResultWithValue::createBasedOnSign(framesRead); +} + +DataCallbackResult FilterAudioStream::onAudioReady(AudioStream *oboeStream, + void *audioData, + int32_t numFrames) { + int32_t framesProcessed; + if (oboeStream->getDirection() == Direction::Output) { + framesProcessed = mFlowGraph->read(audioData, numFrames, 0 /* timeout */); + } else { + framesProcessed = mFlowGraph->write(audioData, numFrames); + } + return (framesProcessed < numFrames) + ? DataCallbackResult::Stop + : mFlowGraph->getDataCallbackResult(); +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_FilterAudioStream_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_FilterAudioStream_android.h new file mode 100644 index 0000000..2b5e3f4 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_FilterAudioStream_android.h @@ -0,0 +1,223 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_FILTER_AUDIO_STREAM_H +#define OBOE_FILTER_AUDIO_STREAM_H + +#include +#include "oboe_oboe_AudioStream_android.h" +#include "oboe_common_DataConversionFlowGraph_android.h" + +namespace oboe { + +/** + * An AudioStream that wraps another AudioStream and provides audio data conversion. + * Operations may include channel conversion, data format conversion and/or sample rate conversion. + */ +class FilterAudioStream : public AudioStream, AudioStreamCallback { +public: + + /** + * Construct an `AudioStream` using the given `AudioStreamBuilder` and a child AudioStream. + * + * This should only be called internally by AudioStreamBuilder. + * Ownership of childStream will be passed to this object. + * + * @param builder containing all the stream's attributes + */ + FilterAudioStream(const AudioStreamBuilder &builder, std::shared_ptr childStream) + : AudioStream(builder) + , mChildStream(childStream) { + // Intercept the callback if used. + if (builder.isErrorCallbackSpecified()) { + mErrorCallback = mChildStream->swapErrorCallback(this); + } + if (builder.isDataCallbackSpecified()) { + mDataCallback = mChildStream->swapDataCallback(this); + } else { + const int size = childStream->getFramesPerBurst() * childStream->getBytesPerFrame(); + mBlockingBuffer = std::make_unique(size); + } + + // Copy parameters that may not match builder. + mBufferCapacityInFrames = mChildStream->getBufferCapacityInFrames(); + mPerformanceMode = mChildStream->getPerformanceMode(); + mSharingMode = mChildStream->getSharingMode(); + mInputPreset = mChildStream->getInputPreset(); + mFramesPerBurst = mChildStream->getFramesPerBurst(); + mDeviceIds = mChildStream->getDeviceIds(); + mHardwareSampleRate = mChildStream->getHardwareSampleRate(); + mHardwareChannelCount = mChildStream->getHardwareChannelCount(); + mHardwareFormat = mChildStream->getHardwareFormat(); + } + + virtual ~FilterAudioStream() = default; + + Result configureFlowGraph(); + + // Close child and parent. + Result close() override { + const Result result1 = mChildStream->close(); + const Result result2 = AudioStream::close(); + return (result1 != Result::OK ? result1 : result2); + } + + /** + * Start the stream asynchronously. Returns immediately (does not block). Equivalent to calling + * `start(0)`. + */ + Result requestStart() override { + return mChildStream->requestStart(); + } + + /** + * Pause the stream asynchronously. Returns immediately (does not block). Equivalent to calling + * `pause(0)`. + */ + Result requestPause() override { + return mChildStream->requestPause(); + } + + /** + * Flush the stream asynchronously. Returns immediately (does not block). Equivalent to calling + * `flush(0)`. + */ + Result requestFlush() override { + return mChildStream->requestFlush(); + } + + /** + * Stop the stream asynchronously. Returns immediately (does not block). Equivalent to calling + * `stop(0)`. + */ + Result requestStop() override { + return mChildStream->requestStop(); + } + + ResultWithValue read(void *buffer, + int32_t numFrames, + int64_t timeoutNanoseconds) override; + + ResultWithValue write(const void *buffer, + int32_t numFrames, + int64_t timeoutNanoseconds) override; + + StreamState getState() override { + return mChildStream->getState(); + } + + Result waitForStateChange( + StreamState inputState, + StreamState *nextState, + int64_t timeoutNanoseconds) override { + return mChildStream->waitForStateChange(inputState, nextState, timeoutNanoseconds); + } + + bool isXRunCountSupported() const override { + return mChildStream->isXRunCountSupported(); + } + + AudioApi getAudioApi() const override { + return mChildStream->getAudioApi(); + } + + void updateFramesWritten() override { + // TODO for output, just count local writes? + mFramesWritten = static_cast(mChildStream->getFramesWritten() * mRateScaler); + } + + void updateFramesRead() override { + // TODO for input, just count local reads? + mFramesRead = static_cast(mChildStream->getFramesRead() * mRateScaler); + } + + void *getUnderlyingStream() const override { + return mChildStream->getUnderlyingStream(); + } + + ResultWithValue setBufferSizeInFrames(int32_t requestedFrames) override { + return mChildStream->setBufferSizeInFrames(requestedFrames); + } + + int32_t getBufferSizeInFrames() override { + mBufferSizeInFrames = mChildStream->getBufferSizeInFrames(); + return mBufferSizeInFrames; + } + + ResultWithValue getXRunCount() override { + return mChildStream->getXRunCount(); + } + + ResultWithValue calculateLatencyMillis() override { + // This will automatically include the latency of the flowgraph? + return mChildStream->calculateLatencyMillis(); + } + + Result getTimestamp(clockid_t clockId, + int64_t *framePosition, + int64_t *timeNanoseconds) override { + int64_t childPosition = 0; + Result result = mChildStream->getTimestamp(clockId, &childPosition, timeNanoseconds); + // It is OK if framePosition is null. + if (framePosition) { + *framePosition = childPosition * mRateScaler; + } + return result; + } + + DataCallbackResult onAudioReady(AudioStream *oboeStream, + void *audioData, + int32_t numFrames) override; + + bool onError(AudioStream * /*audioStream*/, Result error) override { + if (mErrorCallback != nullptr) { + return mErrorCallback->onError(this, error); + } + return false; + } + + void onErrorBeforeClose(AudioStream * /*oboeStream*/, Result error) override { + if (mErrorCallback != nullptr) { + mErrorCallback->onErrorBeforeClose(this, error); + } + } + + void onErrorAfterClose(AudioStream * /*oboeStream*/, Result error) override { + // Close this parent stream because the callback will only close the child. + AudioStream::close(); + if (mErrorCallback != nullptr) { + mErrorCallback->onErrorAfterClose(this, error); + } + } + + /** + * @return last result passed from an error callback + */ + oboe::Result getLastErrorCallbackResult() const override { + return mChildStream->getLastErrorCallbackResult(); + } + +private: + + std::shared_ptr mChildStream; // this stream wraps the child stream + std::unique_ptr mFlowGraph; // for converting data + std::unique_ptr mBlockingBuffer; // temp buffer for write() + double mRateScaler = 1.0; // ratio parent/child sample rates +}; + +} // oboe + +#endif //OBOE_FILTER_AUDIO_STREAM_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_FixedBlockAdapter_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_FixedBlockAdapter_android.cpp new file mode 100644 index 0000000..048af8e --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_FixedBlockAdapter_android.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "oboe_common_FixedBlockAdapter_android.h" + +FixedBlockAdapter::~FixedBlockAdapter() { +} + +int32_t FixedBlockAdapter::open(int32_t bytesPerFixedBlock) +{ + mSize = bytesPerFixedBlock; + mStorage = std::make_unique(bytesPerFixedBlock); + mPosition = 0; + return 0; +} + +int32_t FixedBlockAdapter::close() +{ + mStorage.reset(nullptr); + mSize = 0; + mPosition = 0; + return 0; +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_FixedBlockAdapter_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_FixedBlockAdapter_android.h new file mode 100644 index 0000000..76e961c --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_FixedBlockAdapter_android.h @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAUDIO_FIXED_BLOCK_ADAPTER_H +#define AAUDIO_FIXED_BLOCK_ADAPTER_H + +#include +#include +#include + +/** + * Interface for a class that needs fixed-size blocks. + */ +class FixedBlockProcessor { +public: + virtual ~FixedBlockProcessor() = default; + /** + * + * @param buffer Pointer to first byte of data. + * @param numBytes This will be a fixed size specified in FixedBlockAdapter::open(). + * @return Number of bytes processed or a negative error code. + */ + virtual int32_t onProcessFixedBlock(uint8_t *buffer, int32_t numBytes) = 0; +}; + +/** + * Base class for a variable-to-fixed-size block adapter. + */ +class FixedBlockAdapter +{ +public: + FixedBlockAdapter(FixedBlockProcessor &fixedBlockProcessor) + : mFixedBlockProcessor(fixedBlockProcessor) {} + + virtual ~FixedBlockAdapter(); + + /** + * Allocate internal resources needed for buffering data. + */ + virtual int32_t open(int32_t bytesPerFixedBlock); + + /** + * Free internal resources. + */ + int32_t close(); + +protected: + FixedBlockProcessor &mFixedBlockProcessor; + std::unique_ptr mStorage; // Store data here while assembling buffers. + int32_t mSize = 0; // Size in bytes of the fixed size buffer. + int32_t mPosition = 0; // Offset of the last byte read or written. +}; + +#endif /* AAUDIO_FIXED_BLOCK_ADAPTER_H */ diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_FixedBlockReader_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_FixedBlockReader_android.cpp new file mode 100644 index 0000000..6a94a9a --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_FixedBlockReader_android.cpp @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include "oboe_common_FixedBlockAdapter_android.h" + +#include "oboe_common_FixedBlockReader_android.h" + + +FixedBlockReader::FixedBlockReader(FixedBlockProcessor &fixedBlockProcessor) + : FixedBlockAdapter(fixedBlockProcessor) { + mPosition = mSize; +} + +int32_t FixedBlockReader::open(int32_t bytesPerFixedBlock) { + int32_t result = FixedBlockAdapter::open(bytesPerFixedBlock); + mPosition = 0; + mValid = 0; + return result; +} + +int32_t FixedBlockReader::readFromStorage(uint8_t *buffer, int32_t numBytes) { + int32_t bytesToRead = numBytes; + int32_t dataAvailable = mValid - mPosition; + if (bytesToRead > dataAvailable) { + bytesToRead = dataAvailable; + } + memcpy(buffer, mStorage.get() + mPosition, bytesToRead); + mPosition += bytesToRead; + return bytesToRead; +} + +int32_t FixedBlockReader::read(uint8_t *buffer, int32_t numBytes) { + int32_t bytesRead; + int32_t bytesLeft = numBytes; + while(bytesLeft > 0) { + if (mPosition < mValid) { + // Use up bytes currently in storage. + bytesRead = readFromStorage(buffer, bytesLeft); + buffer += bytesRead; + bytesLeft -= bytesRead; + } else if (bytesLeft >= mSize) { + // Nothing in storage. Read through if enough for a complete block. + bytesRead = mFixedBlockProcessor.onProcessFixedBlock(buffer, mSize); + if (bytesRead < 0) return bytesRead; + buffer += bytesRead; + bytesLeft -= bytesRead; + } else { + // Just need a partial block so we have to reload storage. + bytesRead = mFixedBlockProcessor.onProcessFixedBlock(mStorage.get(), mSize); + if (bytesRead < 0) return bytesRead; + mPosition = 0; + mValid = bytesRead; + if (bytesRead == 0) break; + } + } + return numBytes - bytesLeft; +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_FixedBlockReader_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_FixedBlockReader_android.h new file mode 100644 index 0000000..ffa1eca --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_FixedBlockReader_android.h @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAUDIO_FIXED_BLOCK_READER_H +#define AAUDIO_FIXED_BLOCK_READER_H + +#include + +#include "oboe_common_FixedBlockAdapter_android.h" + +/** + * Read from a fixed-size block to a variable sized block. + * + * This can be used to convert a pull data flow from fixed sized buffers to variable sized buffers. + * An example would be an audio output callback that reads from the app. + */ +class FixedBlockReader : public FixedBlockAdapter +{ +public: + FixedBlockReader(FixedBlockProcessor &fixedBlockProcessor); + + virtual ~FixedBlockReader() = default; + + int32_t open(int32_t bytesPerFixedBlock) override; + + /** + * Read into a variable sized block. + * + * Note that if the fixed-sized blocks must be aligned, then the variable-sized blocks + * must have the same alignment. + * For example, if the fixed-size blocks must be a multiple of 8, then the variable-sized + * blocks must also be a multiple of 8. + * + * @param buffer + * @param numBytes + * @return Number of bytes read or a negative error code. + */ + int32_t read(uint8_t *buffer, int32_t numBytes); + +private: + int32_t readFromStorage(uint8_t *buffer, int32_t numBytes); + + int32_t mValid = 0; // Number of valid bytes in mStorage. +}; + + +#endif /* AAUDIO_FIXED_BLOCK_READER_H */ diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_FixedBlockWriter_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_FixedBlockWriter_android.cpp new file mode 100644 index 0000000..1ef2a6e --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_FixedBlockWriter_android.cpp @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include "oboe_common_FixedBlockAdapter_android.h" +#include "oboe_common_FixedBlockWriter_android.h" + +FixedBlockWriter::FixedBlockWriter(FixedBlockProcessor &fixedBlockProcessor) + : FixedBlockAdapter(fixedBlockProcessor) {} + + +int32_t FixedBlockWriter::writeToStorage(uint8_t *buffer, int32_t numBytes) { + int32_t bytesToStore = numBytes; + int32_t roomAvailable = mSize - mPosition; + if (bytesToStore > roomAvailable) { + bytesToStore = roomAvailable; + } + memcpy(mStorage.get() + mPosition, buffer, bytesToStore); + mPosition += bytesToStore; + return bytesToStore; +} + +int32_t FixedBlockWriter::write(uint8_t *buffer, int32_t numBytes) { + int32_t bytesLeft = numBytes; + + // If we already have data in storage then add to it. + if (mPosition > 0) { + int32_t bytesWritten = writeToStorage(buffer, bytesLeft); + buffer += bytesWritten; + bytesLeft -= bytesWritten; + // If storage full then flush it out + if (mPosition == mSize) { + bytesWritten = mFixedBlockProcessor.onProcessFixedBlock(mStorage.get(), mSize); + if (bytesWritten < 0) return bytesWritten; + mPosition = 0; + if (bytesWritten < mSize) { + // Only some of the data was written! This should not happen. + return -1; + } + } + } + + // Write through if enough for a complete block. + while(bytesLeft > mSize) { + int32_t bytesWritten = mFixedBlockProcessor.onProcessFixedBlock(buffer, mSize); + if (bytesWritten < 0) return bytesWritten; + buffer += bytesWritten; + bytesLeft -= bytesWritten; + } + + // Save any remaining partial blocks for next time. + if (bytesLeft > 0) { + int32_t bytesWritten = writeToStorage(buffer, bytesLeft); + bytesLeft -= bytesWritten; + } + + return numBytes - bytesLeft; +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_FixedBlockWriter_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_FixedBlockWriter_android.h new file mode 100644 index 0000000..b36ab21 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_FixedBlockWriter_android.h @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAUDIO_FIXED_BLOCK_WRITER_H +#define AAUDIO_FIXED_BLOCK_WRITER_H + +#include + +#include "oboe_common_FixedBlockAdapter_android.h" + +/** + * This can be used to convert a push data flow from variable sized buffers to fixed sized buffers. + * An example would be an audio input callback. + */ +class FixedBlockWriter : public FixedBlockAdapter +{ +public: + FixedBlockWriter(FixedBlockProcessor &fixedBlockProcessor); + + virtual ~FixedBlockWriter() = default; + + /** + * Write from a variable sized block. + * + * Note that if the fixed-sized blocks must be aligned, then the variable-sized blocks + * must have the same alignment. + * For example, if the fixed-size blocks must be a multiple of 8, then the variable-sized + * blocks must also be a multiple of 8. + * + * @param buffer + * @param numBytes + * @return Number of bytes written or a negative error code. + */ + int32_t write(uint8_t *buffer, int32_t numBytes); + +private: + + int32_t writeToStorage(uint8_t *buffer, int32_t numBytes); +}; + +#endif /* AAUDIO_FIXED_BLOCK_WRITER_H */ diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_LatencyTuner_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_LatencyTuner_android.cpp new file mode 100644 index 0000000..8aca447 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_LatencyTuner_android.cpp @@ -0,0 +1,108 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "oboe_oboe_LatencyTuner_android.h" + +using namespace oboe; + +LatencyTuner::LatencyTuner(AudioStream &stream) + : LatencyTuner(stream, stream.getBufferCapacityInFrames()) { +} + +LatencyTuner::LatencyTuner(oboe::AudioStream &stream, int32_t maximumBufferSize) + : mStream(stream) + , mMaxBufferSize(maximumBufferSize) { + int32_t burstSize = stream.getFramesPerBurst(); + setMinimumBufferSize(kDefaultNumBursts * burstSize); + setBufferSizeIncrement(burstSize); + reset(); +} + +Result LatencyTuner::tune() { + if (mState == State::Unsupported) { + return Result::ErrorUnimplemented; + } + + Result result = Result::OK; + + // Process reset requests. + int32_t numRequests = mLatencyTriggerRequests.load(); + if (numRequests != mLatencyTriggerResponses.load()) { + mLatencyTriggerResponses.store(numRequests); + reset(); + } + + // Set state to Active if the idle countdown has reached zero. + if (mState == State::Idle && --mIdleCountDown <= 0) { + mState = State::Active; + } + + // When state is Active attempt to change the buffer size if the number of xRuns has increased. + if (mState == State::Active) { + + auto xRunCountResult = mStream.getXRunCount(); + if (xRunCountResult == Result::OK) { + if ((xRunCountResult.value() - mPreviousXRuns) > 0) { + mPreviousXRuns = xRunCountResult.value(); + int32_t oldBufferSize = mStream.getBufferSizeInFrames(); + int32_t requestedBufferSize = oldBufferSize + getBufferSizeIncrement(); + + // Do not request more than the maximum buffer size (which was either user-specified + // or was from stream->getBufferCapacityInFrames()) + if (requestedBufferSize > mMaxBufferSize) requestedBufferSize = mMaxBufferSize; + + // Note that this will not allocate more memory. It simply determines + // how much of the existing buffer capacity will be used. The size will be + // clipped to the bufferCapacity by AAudio. + auto setBufferResult = mStream.setBufferSizeInFrames(requestedBufferSize); + if (setBufferResult != Result::OK) { + result = setBufferResult; + mState = State::Unsupported; + } else if (setBufferResult.value() == oldBufferSize) { + mState = State::AtMax; + } + } + } else { + mState = State::Unsupported; + } + } + + if (mState == State::Unsupported) { + result = Result::ErrorUnimplemented; + } + + if (mState == State::AtMax) { + result = Result::OK; + } + return result; +} + +void LatencyTuner::requestReset() { + if (mState != State::Unsupported) { + mLatencyTriggerRequests++; + } +} + +void LatencyTuner::reset() { + mState = State::Idle; + mIdleCountDown = kIdleCount; + // Set to minimal latency + mStream.setBufferSizeInFrames(getMinimumBufferSize()); +} + +bool LatencyTuner::isAtMaximumBufferSize() { + return mState == State::AtMax; +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_MonotonicCounter_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_MonotonicCounter_android.h new file mode 100644 index 0000000..00c979c --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_MonotonicCounter_android.h @@ -0,0 +1,112 @@ +/* + * Copyright 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef COMMON_MONOTONIC_COUNTER_H +#define COMMON_MONOTONIC_COUNTER_H + +#include + +/** + * Maintain a 64-bit monotonic counter. + * Can be used to track a 32-bit counter that wraps or gets reset. + * + * Note that this is not atomic and has no interior locks. + * A caller will need to provide their own exterior locking + * if they need to use it from multiple threads. + */ +class MonotonicCounter { + +public: + MonotonicCounter() {} + virtual ~MonotonicCounter() {} + + /** + * @return current value of the counter + */ + int64_t get() const { + return mCounter64; + } + + /** + * set the current value of the counter + */ + void set(int64_t counter) { + mCounter64 = counter; + } + + /** + * Advance the counter if delta is positive. + * @return current value of the counter + */ + int64_t increment(int64_t delta) { + if (delta > 0) { + mCounter64 += delta; + } + return mCounter64; + } + + /** + * Advance the 64-bit counter if (current32 - previousCurrent32) > 0. + * This can be used to convert a 32-bit counter that may be wrapping into + * a monotonic 64-bit counter. + * + * This counter32 should NOT be allowed to advance by more than 0x7FFFFFFF between calls. + * Think of the wrapping counter like a sine wave. If the frequency of the signal + * is more than half the sampling rate (Nyquist rate) then you cannot measure it properly. + * If the counter wraps around every 24 hours then we should measure it with a period + * of less than 12 hours. + * + * @return current value of the 64-bit counter + */ + int64_t update32(int32_t counter32) { + int32_t delta = counter32 - mCounter32; + // protect against the mCounter64 going backwards + if (delta > 0) { + mCounter64 += delta; + mCounter32 = counter32; + } + return mCounter64; + } + + /** + * Reset the stored value of the 32-bit counter. + * This is used if your counter32 has been reset to zero. + */ + void reset32() { + mCounter32 = 0; + } + + /** + * Round 64-bit counter up to a multiple of the period. + * + * The period must be positive. + * + * @param period might be, for example, a buffer capacity + */ + void roundUp64(int32_t period) { + if (period > 0) { + int64_t numPeriods = (mCounter64 + period - 1) / period; + mCounter64 = numPeriods * period; + } + } + +private: + int64_t mCounter64 = 0; + int32_t mCounter32 = 0; +}; + + +#endif //COMMON_MONOTONIC_COUNTER_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_OboeDebug_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_OboeDebug_android.h new file mode 100644 index 0000000..dc7434c --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_OboeDebug_android.h @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +#ifndef OBOE_DEBUG_H +#define OBOE_DEBUG_H + +#include + +#ifndef MODULE_NAME +#define MODULE_NAME "OboeAudio" +#endif + +// Always log INFO and errors. +#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, MODULE_NAME, __VA_ARGS__) +#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, MODULE_NAME, __VA_ARGS__) +#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, MODULE_NAME, __VA_ARGS__) +#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL, MODULE_NAME, __VA_ARGS__) + +#if OBOE_ENABLE_LOGGING +#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, MODULE_NAME, __VA_ARGS__) +#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, MODULE_NAME, __VA_ARGS__) +#else +#define LOGV(...) +#define LOGD(...) +#endif + +#endif //OBOE_DEBUG_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_OboeExtensions_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_OboeExtensions_android.cpp new file mode 100644 index 0000000..af6b0b4 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_OboeExtensions_android.cpp @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "oboe_oboe_OboeExtensions_android.h" +#include "oboe_aaudio_AAudioExtensions_android.h" + +using namespace oboe; + +bool OboeExtensions::isMMapSupported(){ + return AAudioExtensions::getInstance().isMMapSupported(); +} + +bool OboeExtensions::isMMapEnabled(){ + return AAudioExtensions::getInstance().isMMapEnabled(); +} + +int32_t OboeExtensions::setMMapEnabled(bool enabled){ + return AAudioExtensions::getInstance().setMMapEnabled(enabled); +} + +bool OboeExtensions::isMMapUsed(oboe::AudioStream *oboeStream){ + return AAudioExtensions::getInstance().isMMapUsed(oboeStream); +} + +bool OboeExtensions::isPartialDataCallbackSupported() { + return AAudioExtensions::getInstance().isPartialDataCallbackSupported(); +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_QuirksManager_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_QuirksManager_android.cpp new file mode 100644 index 0000000..9071258 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_QuirksManager_android.cpp @@ -0,0 +1,319 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "oboe_oboe_AudioStreamBuilder_android.h" +#include "oboe_oboe_Oboe_android.h" +#include "oboe_oboe_Utilities_android.h" + +#include "oboe_common_OboeDebug_android.h" +#include "oboe_common_QuirksManager_android.h" + +using namespace oboe; + +int32_t QuirksManager::DeviceQuirks::clipBufferSize(AudioStream &stream, + int32_t requestedSize) { + if (!OboeGlobals::areWorkaroundsEnabled()) { + return requestedSize; + } + int bottomMargin = kDefaultBottomMarginInBursts; + int topMargin = kDefaultTopMarginInBursts; + if (isMMapUsed(stream)) { + if (stream.getSharingMode() == SharingMode::Exclusive) { + bottomMargin = getExclusiveBottomMarginInBursts(); + topMargin = getExclusiveTopMarginInBursts(); + } + } else { + bottomMargin = kLegacyBottomMarginInBursts; + } + + int32_t burst = stream.getFramesPerBurst(); + int32_t minSize = bottomMargin * burst; + int32_t adjustedSize = requestedSize; + if (adjustedSize < minSize ) { + adjustedSize = minSize; + } else { + int32_t maxSize = stream.getBufferCapacityInFrames() - (topMargin * burst); + if (adjustedSize > maxSize ) { + adjustedSize = maxSize; + } + } + return adjustedSize; +} + +bool QuirksManager::DeviceQuirks::isAAudioMMapPossible(const AudioStreamBuilder &builder) const { + bool isSampleRateCompatible = + builder.getSampleRate() == oboe::Unspecified + || builder.getSampleRate() == kCommonNativeRate + || builder.getSampleRateConversionQuality() != SampleRateConversionQuality::None; + return builder.getPerformanceMode() == PerformanceMode::LowLatency + && isSampleRateCompatible + && builder.getChannelCount() <= kChannelCountStereo; +} + +bool QuirksManager::DeviceQuirks::shouldConvertFloatToI16ForOutputStreams() { + std::string productManufacturer = getPropertyString("ro.product.manufacturer"); + if (getSdkVersion() < __ANDROID_API_L__) { + return true; + } else if ((productManufacturer == "vivo") && (getSdkVersion() < __ANDROID_API_M__)) { + return true; + } + return false; +} + +/** + * This is for Samsung Exynos quirks. Samsung Mobile uses Qualcomm chips so + * the QualcommDeviceQuirks would apply. + */ +class SamsungExynosDeviceQuirks : public QuirksManager::DeviceQuirks { +public: + SamsungExynosDeviceQuirks() { + std::string chipname = getPropertyString("ro.hardware.chipname"); + isExynos9810 = (chipname == "exynos9810"); + isExynos990 = (chipname == "exynos990"); + isExynos850 = (chipname == "exynos850"); + + mBuildChangelist = getPropertyInteger("ro.build.changelist", 0); + } + + virtual ~SamsungExynosDeviceQuirks() = default; + + int32_t getExclusiveBottomMarginInBursts() const override { + return kBottomMargin; + } + + int32_t getExclusiveTopMarginInBursts() const override { + return kTopMargin; + } + + // See Oboe issues #824 and #1247 for more information. + bool isMonoMMapActuallyStereo() const override { + return isExynos9810 || isExynos850; // TODO We can make this version specific if it gets fixed. + } + + bool isAAudioMMapPossible(const AudioStreamBuilder &builder) const override { + return DeviceQuirks::isAAudioMMapPossible(builder) + // Samsung says they use Legacy for Camcorder + && builder.getInputPreset() != oboe::InputPreset::Camcorder; + } + + bool isMMapSafe(const AudioStreamBuilder &builder) override { + const bool isInput = builder.getDirection() == Direction::Input; + // This detects b/159066712 , S20 LSI has corrupt low latency audio recording + // and turns off MMAP. + // See also https://github.com/google/oboe/issues/892 + bool isRecordingCorrupted = isInput + && isExynos990 + && mBuildChangelist < 19350896; + + // Certain S9+ builds record silence when using MMAP and not using the VoiceCommunication + // preset. + // See https://github.com/google/oboe/issues/1110 + bool wouldRecordSilence = isInput + && isExynos9810 + && mBuildChangelist <= 18847185 + && (builder.getInputPreset() != InputPreset::VoiceCommunication); + + if (wouldRecordSilence){ + LOGI("QuirksManager::%s() Requested stream configuration would result in silence on " + "this device. Switching off MMAP.", __func__); + } + + return !isRecordingCorrupted && !wouldRecordSilence; + } + +private: + // Stay farther away from DSP position on Exynos devices. + static constexpr int32_t kBottomMargin = 2; + static constexpr int32_t kTopMargin = 1; + bool isExynos9810 = false; + bool isExynos990 = false; + bool isExynos850 = false; + int mBuildChangelist = 0; +}; + +class QualcommDeviceQuirks : public QuirksManager::DeviceQuirks { +public: + QualcommDeviceQuirks() { + std::string modelName = getPropertyString("ro.soc.model"); + isSM8150 = (modelName == "SDM8150"); + } + + virtual ~QualcommDeviceQuirks() = default; + + int32_t getExclusiveBottomMarginInBursts() const override { + return kBottomMargin; + } + + bool isMMapSafe(const AudioStreamBuilder &builder) override { + // See https://github.com/google/oboe/issues/1121#issuecomment-897957749 + bool isMMapBroken = false; + if (isSM8150 && (getSdkVersion() <= __ANDROID_API_P__)) { + LOGI("QuirksManager::%s() MMAP not actually supported on this chip." + " Switching off MMAP.", __func__); + isMMapBroken = true; + } + + return !isMMapBroken; + } + +private: + bool isSM8150 = false; + static constexpr int32_t kBottomMargin = 1; +}; + +QuirksManager::QuirksManager() { + std::string productManufacturer = getPropertyString("ro.product.manufacturer"); + if (productManufacturer == "samsung") { + std::string arch = getPropertyString("ro.arch"); + bool isExynos = (arch.rfind("exynos", 0) == 0); // starts with? + if (isExynos) { + mDeviceQuirks = std::make_unique(); + } + } + if (!mDeviceQuirks) { + std::string socManufacturer = getPropertyString("ro.soc.manufacturer"); + if (socManufacturer == "Qualcomm") { + // This may include Samsung Mobile devices. + mDeviceQuirks = std::make_unique(); + } else { + mDeviceQuirks = std::make_unique(); + } + } +} + +bool QuirksManager::isConversionNeeded( + const AudioStreamBuilder &builder, + AudioStreamBuilder &childBuilder) { + bool conversionNeeded = false; + const bool isLowLatency = builder.getPerformanceMode() == PerformanceMode::LowLatency; + const bool isInput = builder.getDirection() == Direction::Input; + const bool isFloat = builder.getFormat() == AudioFormat::Float; + const bool isIEC61937 = builder.getFormat() == AudioFormat::IEC61937; + const bool isCompressed = isCompressedFormat(builder.getFormat()); + + // There should be no conversion for IEC61937. Sample rates and channel counts must be set explicitly. + if (isIEC61937) { + LOGI("QuirksManager::%s() conversion not needed for IEC61937", __func__); + return false; + } + + if (isCompressed) { + LOGI("QuirksManager::%s() conversion not needed for compressed format %d", + __func__, builder.getFormat()); + return false; + } + + // There are multiple bugs involving using callback with a specified callback size. + // Issue #778: O to Q had a problem with Legacy INPUT streams for FLOAT streams + // and a specified callback size. It would assert because of a bad buffer size. + // + // Issue #973: O to R had a problem with Legacy output streams using callback and a specified callback size. + // An AudioTrack stream could still be running when the AAudio FixedBlockReader was closed. + // Internally b/161914201#comment25 + // + // Issue #983: O to R would glitch if the framesPerCallback was too small. + // + // Most of these problems were related to Legacy stream. MMAP was OK. But we don't + // know if we will get an MMAP stream. So, to be safe, just do the conversion in Oboe. + if (OboeGlobals::areWorkaroundsEnabled() + && builder.willUseAAudio() + && builder.isDataCallbackSpecified() + && builder.getFramesPerDataCallback() != 0 + && getSdkVersion() <= __ANDROID_API_R__) { + LOGI("QuirksManager::%s() avoid setFramesPerCallback(n>0)", __func__); + childBuilder.setFramesPerCallback(oboe::Unspecified); + conversionNeeded = true; + } + + // If a SAMPLE RATE is specified for low latency, let the native code choose an optimal rate. + // This isn't really a workaround. It is an Oboe feature that is convenient to place here. + // TODO There may be a problem if the devices supports low latency + // at a higher rate than the default. + if (builder.getSampleRate() != oboe::Unspecified + && builder.getSampleRateConversionQuality() != SampleRateConversionQuality::None + && isLowLatency + ) { + childBuilder.setSampleRate(oboe::Unspecified); // native API decides the best sample rate + conversionNeeded = true; + } + + // Data Format + // OpenSL ES and AAudio before P do not support FAST path for FLOAT capture. + if (OboeGlobals::areWorkaroundsEnabled() + && isFloat + && isInput + && builder.isFormatConversionAllowed() + && isLowLatency + && (!builder.willUseAAudio() || (getSdkVersion() < __ANDROID_API_P__)) + ) { + childBuilder.setFormat(AudioFormat::I16); // needed for FAST track + conversionNeeded = true; + LOGI("QuirksManager::%s() forcing internal format to I16 for low latency", __func__); + } + + // Add quirk for float output when needed. + if (OboeGlobals::areWorkaroundsEnabled() + && isFloat + && !isInput + && builder.isFormatConversionAllowed() + && mDeviceQuirks->shouldConvertFloatToI16ForOutputStreams() + ) { + childBuilder.setFormat(AudioFormat::I16); + conversionNeeded = true; + LOGI("QuirksManager::%s() float was requested but not supported on pre-L devices " + "and some devices like Vivo devices may have issues on L devices, " + "creating an underlying I16 stream and using format conversion to provide a float " + "stream", __func__); + } + + // Channel Count conversions + if (OboeGlobals::areWorkaroundsEnabled() + && builder.isChannelConversionAllowed() + && builder.getChannelCount() == kChannelCountStereo + && isInput + && isLowLatency + && (!builder.willUseAAudio() && (getSdkVersion() == __ANDROID_API_O__)) + ) { + // Workaround for heap size regression in O. + // b/66967812 AudioRecord does not allow FAST track for stereo capture in O + childBuilder.setChannelCount(kChannelCountMono); + conversionNeeded = true; + LOGI("QuirksManager::%s() using mono internally for low latency on O", __func__); + } else if (OboeGlobals::areWorkaroundsEnabled() + && builder.getChannelCount() == kChannelCountMono + && isInput + && mDeviceQuirks->isMonoMMapActuallyStereo() + && builder.willUseAAudio() + // Note: we might use this workaround on a device that supports + // MMAP but will use Legacy for this stream. But this will only happen + // on devices that have the broken mono. + && mDeviceQuirks->isAAudioMMapPossible(builder) + ) { + // Workaround for mono actually running in stereo mode. + childBuilder.setChannelCount(kChannelCountStereo); // Use stereo and extract first channel. + conversionNeeded = true; + LOGI("QuirksManager::%s() using stereo internally to avoid broken mono", __func__); + } + // Note that MMAP does not support mono in 8.1. But that would only matter on Pixel 1 + // phones and they have almost all been updated to 9.0. + + return conversionNeeded; +} + +bool QuirksManager::isMMapSafe(AudioStreamBuilder &builder) { + if (!OboeGlobals::areWorkaroundsEnabled()) return true; + return mDeviceQuirks->isMMapSafe(builder); +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_QuirksManager_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_QuirksManager_android.h new file mode 100644 index 0000000..c1ead82 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_QuirksManager_android.h @@ -0,0 +1,134 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_QUIRKS_MANAGER_H +#define OBOE_QUIRKS_MANAGER_H + +#include +#include "oboe_oboe_AudioStreamBuilder_android.h" +#include "oboe_aaudio_AudioStreamAAudio_android.h" + +#ifndef __ANDROID_API_R__ +#define __ANDROID_API_R__ 30 +#endif + +namespace oboe { + +/** + * INTERNAL USE ONLY. + * + * Based on manufacturer, model and Android version number + * decide whether data conversion needs to occur. + * + * This also manages device and version specific workarounds. + */ + +class QuirksManager { +public: + + static QuirksManager &getInstance() { + static QuirksManager instance; // singleton + return instance; + } + + QuirksManager(); + virtual ~QuirksManager() = default; + + /** + * Do we need to do channel, format or rate conversion to provide a low latency + * stream for this builder? If so then provide a builder for the native child stream + * that will be used to get low latency. + * + * @param builder builder provided by application + * @param childBuilder modified builder appropriate for the underlying device + * @return true if conversion is needed + */ + bool isConversionNeeded(const AudioStreamBuilder &builder, AudioStreamBuilder &childBuilder); + + static bool isMMapUsed(AudioStream &stream) { + bool answer = false; + if (stream.getAudioApi() == AudioApi::AAudio) { + AudioStreamAAudio *streamAAudio = + reinterpret_cast(&stream); + answer = streamAAudio->isMMapUsed(); + } + return answer; + } + + virtual int32_t clipBufferSize(AudioStream &stream, int32_t bufferSize) { + return mDeviceQuirks->clipBufferSize(stream, bufferSize); + } + + class DeviceQuirks { + public: + virtual ~DeviceQuirks() = default; + + /** + * Restrict buffer size. This is mainly to avoid glitches caused by MMAP + * timestamp inaccuracies. + * @param stream + * @param requestedSize + * @return + */ + int32_t clipBufferSize(AudioStream &stream, int32_t requestedSize); + + // Exclusive MMAP streams can have glitches because they are using a timing + // model of the DSP to control IO instead of direct synchronization. + virtual int32_t getExclusiveBottomMarginInBursts() const { + return kDefaultBottomMarginInBursts; + } + + virtual int32_t getExclusiveTopMarginInBursts() const { + return kDefaultTopMarginInBursts; + } + + // On some devices, you can open a mono stream but it is actually running in stereo! + virtual bool isMonoMMapActuallyStereo() const { + return false; + } + + virtual bool isAAudioMMapPossible(const AudioStreamBuilder &builder) const; + + virtual bool isMMapSafe(const AudioStreamBuilder & /* builder */ ) { + return true; + } + + // On some devices, Float does not work so it should be converted to I16. + static bool shouldConvertFloatToI16ForOutputStreams(); + + static constexpr int32_t kDefaultBottomMarginInBursts = 0; + static constexpr int32_t kDefaultTopMarginInBursts = 0; + + // For Legacy streams, do not let the buffer go below one burst. + // b/129545119 | AAudio Legacy allows setBufferSizeInFrames too low + // Fixed in Q + static constexpr int32_t kLegacyBottomMarginInBursts = 1; + static constexpr int32_t kCommonNativeRate = 48000; // very typical native sample rate + }; + + bool isMMapSafe(AudioStreamBuilder &builder); + +private: + + static constexpr int32_t kChannelCountMono = 1; + static constexpr int32_t kChannelCountStereo = 2; + + std::unique_ptr mDeviceQuirks{}; + +}; + +} +#endif //OBOE_QUIRKS_MANAGER_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_SourceFloatCaller_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_SourceFloatCaller_android.cpp new file mode 100644 index 0000000..bcba239 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_SourceFloatCaller_android.cpp @@ -0,0 +1,30 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include "oboe_flowgraph_FlowGraphNode_android.h" +#include "oboe_common_SourceFloatCaller_android.h" + +using namespace oboe; +using namespace flowgraph; + +int32_t SourceFloatCaller::onProcess(int32_t numFrames) { + int32_t numBytes = mStream->getBytesPerFrame() * numFrames; + int32_t bytesRead = mBlockReader.read((uint8_t *) output.getBuffer(), numBytes); + int32_t framesRead = bytesRead / mStream->getBytesPerFrame(); + return framesRead; +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_SourceFloatCaller_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_SourceFloatCaller_android.h new file mode 100644 index 0000000..52924f1 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_SourceFloatCaller_android.h @@ -0,0 +1,44 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_SOURCE_FLOAT_CALLER_H +#define OBOE_SOURCE_FLOAT_CALLER_H + +#include +#include + +#include "oboe_flowgraph_FlowGraphNode_android.h" +#include "oboe_common_AudioSourceCaller_android.h" +#include "oboe_common_FixedBlockReader_android.h" + +namespace oboe { +/** + * AudioSource that uses callback to get more float data. + */ +class SourceFloatCaller : public AudioSourceCaller { +public: + SourceFloatCaller(int32_t channelCount, int32_t framesPerCallback) + : AudioSourceCaller(channelCount, framesPerCallback, (int32_t)sizeof(float)) {} + + int32_t onProcess(int32_t numFrames) override; + + const char *getName() override { + return "SourceFloatCaller"; + } +}; + +} +#endif //OBOE_SOURCE_FLOAT_CALLER_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_SourceI16Caller_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_SourceI16Caller_android.cpp new file mode 100644 index 0000000..716469d --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_SourceI16Caller_android.cpp @@ -0,0 +1,47 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include "oboe_flowgraph_FlowGraphNode_android.h" +#include "oboe_common_SourceI16Caller_android.h" + +#if FLOWGRAPH_ANDROID_INTERNAL +#include +#endif + +using namespace oboe; +using namespace flowgraph; + +int32_t SourceI16Caller::onProcess(int32_t numFrames) { + int32_t numBytes = mStream->getBytesPerFrame() * numFrames; + int32_t bytesRead = mBlockReader.read((uint8_t *) mConversionBuffer.get(), numBytes); + int32_t framesRead = bytesRead / mStream->getBytesPerFrame(); + + float *floatData = output.getBuffer(); + const int16_t *shortData = mConversionBuffer.get(); + int32_t numSamples = framesRead * output.getSamplesPerFrame(); + +#if FLOWGRAPH_ANDROID_INTERNAL + memcpy_to_float_from_i16(floatData, shortData, numSamples); +#else + for (int i = 0; i < numSamples; i++) { + *floatData++ = *shortData++ * (1.0f / 32768); + } +#endif + + return framesRead; +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_SourceI16Caller_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_SourceI16Caller_android.h new file mode 100644 index 0000000..ee6f477 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_SourceI16Caller_android.h @@ -0,0 +1,49 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_SOURCE_I16_CALLER_H +#define OBOE_SOURCE_I16_CALLER_H + +#include +#include + +#include "oboe_flowgraph_FlowGraphNode_android.h" +#include "oboe_common_AudioSourceCaller_android.h" +#include "oboe_common_FixedBlockReader_android.h" + +namespace oboe { +/** + * AudioSource that uses callback to get more data. + */ +class SourceI16Caller : public AudioSourceCaller { +public: + SourceI16Caller(int32_t channelCount, int32_t framesPerCallback) + : AudioSourceCaller(channelCount, framesPerCallback, sizeof(int16_t)) { + mConversionBuffer = std::make_unique(static_cast(channelCount) + * static_cast(output.getFramesPerBuffer())); + } + + int32_t onProcess(int32_t numFrames) override; + + const char *getName() override { + return "SourceI16Caller"; + } +private: + std::unique_ptr mConversionBuffer; +}; + +} +#endif //OBOE_SOURCE_I16_CALLER_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_SourceI24Caller_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_SourceI24Caller_android.cpp new file mode 100644 index 0000000..ee40b8f --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_SourceI24Caller_android.cpp @@ -0,0 +1,56 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include "oboe_flowgraph_FlowGraphNode_android.h" +#include "oboe_common_SourceI24Caller_android.h" + +#if FLOWGRAPH_ANDROID_INTERNAL +#include +#endif + +using namespace oboe; +using namespace flowgraph; + +int32_t SourceI24Caller::onProcess(int32_t numFrames) { + int32_t numBytes = mStream->getBytesPerFrame() * numFrames; + int32_t bytesRead = mBlockReader.read((uint8_t *) mConversionBuffer.get(), numBytes); + int32_t framesRead = bytesRead / mStream->getBytesPerFrame(); + + float *floatData = output.getBuffer(); + const uint8_t *byteData = mConversionBuffer.get(); + int32_t numSamples = framesRead * output.getSamplesPerFrame(); + +#if FLOWGRAPH_ANDROID_INTERNAL + memcpy_to_float_from_p24(floatData, byteData, numSamples); +#else + static const float scale = 1. / (float)(1UL << 31); + for (int i = 0; i < numSamples; i++) { + // Assemble the data assuming Little Endian format. + int32_t pad = byteData[2]; + pad <<= 8; + pad |= byteData[1]; + pad <<= 8; + pad |= byteData[0]; + pad <<= 8; // Shift to 32 bit data so the sign is correct. + byteData += kBytesPerI24Packed; + *floatData++ = pad * scale; // scale to range -1.0 to 1.0 + } +#endif + + return framesRead; +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_SourceI24Caller_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_SourceI24Caller_android.h new file mode 100644 index 0000000..2cc8952 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_SourceI24Caller_android.h @@ -0,0 +1,53 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_SOURCE_I24_CALLER_H +#define OBOE_SOURCE_I24_CALLER_H + +#include +#include + +#include "oboe_flowgraph_FlowGraphNode_android.h" +#include "oboe_common_AudioSourceCaller_android.h" +#include "oboe_common_FixedBlockReader_android.h" + +namespace oboe { + +/** + * AudioSource that uses callback to get more data. + */ +class SourceI24Caller : public AudioSourceCaller { +public: + SourceI24Caller(int32_t channelCount, int32_t framesPerCallback) + : AudioSourceCaller(channelCount, framesPerCallback, kBytesPerI24Packed) { + mConversionBuffer = std::make_unique(static_cast(kBytesPerI24Packed) + * static_cast(channelCount) + * static_cast(output.getFramesPerBuffer())); + } + + int32_t onProcess(int32_t numFrames) override; + + const char *getName() override { + return "SourceI24Caller"; + } + +private: + std::unique_ptr mConversionBuffer; + static constexpr int kBytesPerI24Packed = 3; +}; + +} +#endif //OBOE_SOURCE_I16_CALLER_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_SourceI32Caller_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_SourceI32Caller_android.cpp new file mode 100644 index 0000000..7b33227 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_SourceI32Caller_android.cpp @@ -0,0 +1,47 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include "oboe_flowgraph_FlowGraphNode_android.h" +#include "oboe_common_SourceI32Caller_android.h" + +#if FLOWGRAPH_ANDROID_INTERNAL +#include +#endif + +using namespace oboe; +using namespace flowgraph; + +int32_t SourceI32Caller::onProcess(int32_t numFrames) { + int32_t numBytes = mStream->getBytesPerFrame() * numFrames; + int32_t bytesRead = mBlockReader.read((uint8_t *) mConversionBuffer.get(), numBytes); + int32_t framesRead = bytesRead / mStream->getBytesPerFrame(); + + float *floatData = output.getBuffer(); + const int32_t *intData = mConversionBuffer.get(); + int32_t numSamples = framesRead * output.getSamplesPerFrame(); + +#if FLOWGRAPH_ANDROID_INTERNAL + memcpy_to_float_from_i32(floatData, shortData, numSamples); +#else + for (int i = 0; i < numSamples; i++) { + *floatData++ = *intData++ * kScale; + } +#endif + + return framesRead; +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_SourceI32Caller_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_SourceI32Caller_android.h new file mode 100644 index 0000000..2e2b163 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_SourceI32Caller_android.h @@ -0,0 +1,53 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_SOURCE_I32_CALLER_H +#define OBOE_SOURCE_I32_CALLER_H + +#include +#include +#include + +#include "oboe_flowgraph_FlowGraphNode_android.h" +#include "oboe_common_AudioSourceCaller_android.h" +#include "oboe_common_FixedBlockReader_android.h" + +namespace oboe { + +/** + * AudioSource that uses callback to get more data. + */ +class SourceI32Caller : public AudioSourceCaller { +public: + SourceI32Caller(int32_t channelCount, int32_t framesPerCallback) + : AudioSourceCaller(channelCount, framesPerCallback, sizeof(int32_t)) { + mConversionBuffer = std::make_unique(static_cast(channelCount) + * static_cast(output.getFramesPerBuffer())); + } + + int32_t onProcess(int32_t numFrames) override; + + const char *getName() override { + return "SourceI32Caller"; + } + +private: + std::unique_ptr mConversionBuffer; + static constexpr float kScale = 1.0 / (1UL << 31); +}; + +} +#endif //OBOE_SOURCE_I32_CALLER_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_StabilizedCallback_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_StabilizedCallback_android.cpp new file mode 100644 index 0000000..c404579 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_StabilizedCallback_android.cpp @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "oboe_common_Trace_android.h" +#include "oboe_oboe_AudioClock_android.h" +#include "oboe_oboe_StabilizedCallback_android.h" + +constexpr int32_t kLoadGenerationStepSizeNanos = 20000; +constexpr float kPercentageOfCallbackToUse = 0.8; + +using namespace oboe; + +StabilizedCallback::StabilizedCallback(AudioStreamCallback *callback) : mCallback(callback) { +} + +/** + * An audio callback which attempts to do work for a fixed amount of time. + * + * @param oboeStream + * @param audioData + * @param numFrames + * @return + */ +DataCallbackResult +StabilizedCallback::onAudioReady(AudioStream *oboeStream, void *audioData, int32_t numFrames) { + + int64_t startTimeNanos = AudioClock::getNanoseconds(); + + if (mFrameCount == 0){ + mEpochTimeNanos = startTimeNanos; + } + + int64_t durationSinceEpochNanos = startTimeNanos - mEpochTimeNanos; + + // In an ideal world the callback start time will be exactly the same as the duration of the + // frames already read/written into the stream. In reality the callback can start early + // or late. By finding the delta we can calculate the target duration for our stabilized + // callback. + int64_t idealStartTimeNanos = (mFrameCount * kNanosPerSecond) / oboeStream->getSampleRate(); + int64_t lateStartNanos = durationSinceEpochNanos - idealStartTimeNanos; + + if (lateStartNanos < 0){ + // This was an early start which indicates that our previous epoch was a late callback. + // Update our epoch to this more accurate time. + mEpochTimeNanos = startTimeNanos; + mFrameCount = 0; + } + + int64_t numFramesAsNanos = (numFrames * kNanosPerSecond) / oboeStream->getSampleRate(); + int64_t targetDurationNanos = static_cast( + (numFramesAsNanos * kPercentageOfCallbackToUse) - lateStartNanos); + + bool traceEnabled = Trace::getInstance().isEnabled(); + if (traceEnabled) Trace::getInstance().beginSection("Actual load"); + DataCallbackResult result = mCallback->onAudioReady(oboeStream, audioData, numFrames); + if (traceEnabled) Trace::getInstance().endSection(); + + int64_t executionDurationNanos = AudioClock::getNanoseconds() - startTimeNanos; + int64_t stabilizingLoadDurationNanos = targetDurationNanos - executionDurationNanos; + + if (traceEnabled) Trace::getInstance().beginSection("Stabilized load for %lldns", stabilizingLoadDurationNanos); + generateLoad(stabilizingLoadDurationNanos); + if (traceEnabled) Trace::getInstance().endSection(); + + // Wraparound: At 48000 frames per second mFrameCount wraparound will occur after 6m years, + // significantly longer than the average lifetime of an Android phone. + mFrameCount += numFrames; + return result; +} + +void StabilizedCallback::generateLoad(int64_t durationNanos) { + + int64_t currentTimeNanos = AudioClock::getNanoseconds(); + int64_t deadlineTimeNanos = currentTimeNanos + durationNanos; + + // opsPerStep gives us an estimated number of operations which need to be run to fully utilize + // the CPU for a fixed amount of time (specified by kLoadGenerationStepSizeNanos). + // After each step the opsPerStep value is re-calculated based on the actual time taken to + // execute those operations. + auto opsPerStep = (int)(mOpsPerNano * kLoadGenerationStepSizeNanos); + int64_t stepDurationNanos = 0; + int64_t previousTimeNanos = 0; + + while (currentTimeNanos <= deadlineTimeNanos){ + + for (int i = 0; i < opsPerStep; i++) cpu_relax(); + + previousTimeNanos = currentTimeNanos; + currentTimeNanos = AudioClock::getNanoseconds(); + stepDurationNanos = currentTimeNanos - previousTimeNanos; + + // Calculate exponential moving average to smooth out values, this acts as a low pass filter. + // @see https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average + static const float kFilterCoefficient = 0.1; + auto measuredOpsPerNano = (double) opsPerStep / stepDurationNanos; + mOpsPerNano = kFilterCoefficient * measuredOpsPerNano + (1.0 - kFilterCoefficient) * mOpsPerNano; + opsPerStep = (int) (mOpsPerNano * kLoadGenerationStepSizeNanos); + } +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_Trace_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_Trace_android.cpp new file mode 100644 index 0000000..71ab8f1 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_Trace_android.cpp @@ -0,0 +1,94 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include "oboe_common_Trace_android.h" +#include "oboe_common_OboeDebug_android.h" + +using namespace oboe; + +typedef void *(*fp_ATrace_beginSection)(const char *sectionName); + +typedef void *(*fp_ATrace_endSection)(); + +typedef void *(*fp_ATrace_setCounter)(const char *counterName, int64_t counterValue); + +typedef bool *(*fp_ATrace_isEnabled)(void); + + +bool Trace::isEnabled() const { + return ATrace_isEnabled != nullptr && ATrace_isEnabled(); +} + +void Trace::beginSection(const char *format, ...) { + char buffer[256]; + va_list va; + va_start(va, format); + vsprintf(buffer, format, va); + ATrace_beginSection(buffer); + va_end(va); +} + +void Trace::endSection() const { + ATrace_endSection(); +} + +void Trace::setCounter(const char *counterName, int64_t counterValue) const { + ATrace_setCounter(counterName, counterValue); +} + +Trace::Trace() { + // Using dlsym allows us to use tracing on API 21+ without needing android/trace.h which wasn't + // published until API 23 + void *lib = dlopen("libandroid.so", RTLD_NOW | RTLD_LOCAL); + LOGD("Trace(): dlopen(%s) returned %p", "libandroid.so", lib); + if (lib == nullptr) { + LOGE("Trace() could not open libandroid.so to dynamically load tracing symbols"); + } else { + ATrace_beginSection = + reinterpret_cast( + dlsym(lib, "ATrace_beginSection")); + if (ATrace_beginSection == nullptr) { + LOGE("Trace::beginSection() not supported"); + return; + } + + ATrace_endSection = + reinterpret_cast( + dlsym(lib, "ATrace_endSection")); + if (ATrace_endSection == nullptr) { + LOGE("Trace::endSection() not supported"); + return; + } + + ATrace_setCounter = + reinterpret_cast( + dlsym(lib, "ATrace_setCounter")); + if (ATrace_setCounter == nullptr) { + LOGE("Trace::setCounter() not supported"); + return; + } + + // If any of the previous functions are null then ATrace_isEnabled will be null. + ATrace_isEnabled = + reinterpret_cast( + dlsym(lib, "ATrace_isEnabled")); + if (ATrace_isEnabled == nullptr) { + LOGE("Trace::isEnabled() not supported"); + } + } +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_Trace_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_Trace_android.h new file mode 100644 index 0000000..d4a48ea --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_Trace_android.h @@ -0,0 +1,72 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_TRACE_H +#define OBOE_TRACE_H + +#include + +namespace oboe { + +/** + * Wrapper for tracing use with Perfetto + */ +class Trace { + +public: + static Trace &getInstance() { + static Trace instance; + return instance; + } + + /** + * @return true if Perfetto tracing is enabled. + */ + bool isEnabled() const; + + /** + * Only call this function if isEnabled() returns true. + * @param format + * @param ... + */ + void beginSection(const char *format, ...); + + /** + * Only call this function if isEnabled() returns true. + */ + void endSection() const; + + /** + * Only call this function if isEnabled() returns true. + * @param counterName human readable name + * @param counterValue value to log in trace + */ + void setCounter(const char *counterName, int64_t counterValue) const; + +private: + Trace(); +// Tracing functions + void *(*ATrace_beginSection)(const char *sectionName) = nullptr; + + void *(*ATrace_endSection)() = nullptr; + + void *(*ATrace_setCounter)(const char *counterName, int64_t counterValue) = nullptr; + + bool *(*ATrace_isEnabled)(void) = nullptr; +}; + +} +#endif //OBOE_TRACE_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_Utilities_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_Utilities_android.cpp new file mode 100644 index 0000000..d8899c3 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_Utilities_android.cpp @@ -0,0 +1,403 @@ +/* + * Copyright 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +#include +#include +#include +#include + +#ifdef __ANDROID__ +#include +#endif + +#include "oboe_oboe_AudioStream_android.h" +#include "oboe_oboe_Definitions_android.h" +#include "oboe_oboe_Utilities_android.h" + +namespace oboe { + +constexpr float kScaleI16ToFloat = (1.0f / 32768.0f); + +void convertFloatToPcm16(const float *source, int16_t *destination, int32_t numSamples) { + for (int i = 0; i < numSamples; i++) { + float fval = source[i]; + fval += 1.0; // to avoid discontinuity at 0.0 caused by truncation + fval *= 32768.0f; + auto sample = static_cast(fval); + // clip to 16-bit range + if (sample < 0) sample = 0; + else if (sample > 0x0FFFF) sample = 0x0FFFF; + sample -= 32768; // center at zero + destination[i] = static_cast(sample); + } +} + +void convertPcm16ToFloat(const int16_t *source, float *destination, int32_t numSamples) { + for (int i = 0; i < numSamples; i++) { + destination[i] = source[i] * kScaleI16ToFloat; + } +} + +int32_t convertFormatToSizeInBytes(AudioFormat format) { + int32_t size = 0; + switch (format) { + case AudioFormat::I16: + size = sizeof(int16_t); + break; + case AudioFormat::Float: + size = sizeof(float); + break; + case AudioFormat::I24: + size = 3; // packed 24-bit data + break; + case AudioFormat::I32: + size = sizeof(int32_t); + break; + case AudioFormat::IEC61937: + size = sizeof(int16_t); + break; + case AudioFormat::MP3: + case AudioFormat::AAC_LC: + case AudioFormat::AAC_HE_V1: + case AudioFormat::AAC_HE_V2: + case AudioFormat::AAC_ELD: + case AudioFormat::AAC_XHE: + case AudioFormat::OPUS: + // For compressed formats, set the size per sample as 0 as they may not have + // fix size per sample. + size = 0; + break; + default: + break; + } + return size; +} + +template<> +const char *convertToText(Result returnCode) { + switch (returnCode) { + case Result::OK: return "OK"; + case Result::ErrorDisconnected: return "ErrorDisconnected"; + case Result::ErrorIllegalArgument: return "ErrorIllegalArgument"; + case Result::ErrorInternal: return "ErrorInternal"; + case Result::ErrorInvalidState: return "ErrorInvalidState"; + case Result::ErrorInvalidHandle: return "ErrorInvalidHandle"; + case Result::ErrorUnimplemented: return "ErrorUnimplemented"; + case Result::ErrorUnavailable: return "ErrorUnavailable"; + case Result::ErrorNoFreeHandles: return "ErrorNoFreeHandles"; + case Result::ErrorNoMemory: return "ErrorNoMemory"; + case Result::ErrorNull: return "ErrorNull"; + case Result::ErrorTimeout: return "ErrorTimeout"; + case Result::ErrorWouldBlock: return "ErrorWouldBlock"; + case Result::ErrorInvalidFormat: return "ErrorInvalidFormat"; + case Result::ErrorOutOfRange: return "ErrorOutOfRange"; + case Result::ErrorNoService: return "ErrorNoService"; + case Result::ErrorInvalidRate: return "ErrorInvalidRate"; + case Result::ErrorClosed: return "ErrorClosed"; + default: return "Unrecognized result"; + } +} + +template<> +const char *convertToText(AudioFormat format) { + switch (format) { + case AudioFormat::Invalid: return "Invalid"; + case AudioFormat::Unspecified: return "Unspecified"; + case AudioFormat::I16: return "I16"; + case AudioFormat::Float: return "Float"; + case AudioFormat::I24: return "I24"; + case AudioFormat::I32: return "I32"; + case AudioFormat::IEC61937: return "IEC61937"; + case AudioFormat::MP3: return "MP3"; + case AudioFormat::AAC_LC: return "AAC_LC"; + case AudioFormat::AAC_HE_V1: return "AAC_HE_V1"; + case AudioFormat::AAC_HE_V2: return "AAC_HE_V2"; + case AudioFormat::AAC_ELD: return "AAC_ELD"; + case AudioFormat::AAC_XHE: return "AAC_XHE"; + case AudioFormat::OPUS: return "OPUS"; + default: return "Unrecognized format"; + } +} + +template<> +const char *convertToText(PerformanceMode mode) { + switch (mode) { + case PerformanceMode::LowLatency: return "LowLatency"; + case PerformanceMode::None: return "None"; + case PerformanceMode::PowerSaving: return "PowerSaving"; + default: return "Unrecognized performance mode"; + } +} + +template<> +const char *convertToText(SharingMode mode) { + switch (mode) { + case SharingMode::Exclusive: return "Exclusive"; + case SharingMode::Shared: return "Shared"; + default: return "Unrecognized sharing mode"; + } +} + +template<> +const char *convertToText(DataCallbackResult result) { + switch (result) { + case DataCallbackResult::Continue: return "Continue"; + case DataCallbackResult::Stop: return "Stop"; + default: return "Unrecognized data callback result"; + } +} + +template<> +const char *convertToText(Direction direction) { + switch (direction) { + case Direction::Input: return "Input"; + case Direction::Output: return "Output"; + default: return "Unrecognized direction"; + } +} + +template<> +const char *convertToText(StreamState state) { + switch (state) { + case StreamState::Closed: return "Closed"; + case StreamState::Closing: return "Closing"; + case StreamState::Disconnected: return "Disconnected"; + case StreamState::Flushed: return "Flushed"; + case StreamState::Flushing: return "Flushing"; + case StreamState::Open: return "Open"; + case StreamState::Paused: return "Paused"; + case StreamState::Pausing: return "Pausing"; + case StreamState::Started: return "Started"; + case StreamState::Starting: return "Starting"; + case StreamState::Stopped: return "Stopped"; + case StreamState::Stopping: return "Stopping"; + case StreamState::Uninitialized: return "Uninitialized"; + case StreamState::Unknown: return "Unknown"; + default: return "Unrecognized stream state"; + } +} + +template<> +const char *convertToText(AudioApi audioApi) { + + switch (audioApi) { + case AudioApi::Unspecified: return "Unspecified"; + case AudioApi::OpenSLES: return "OpenSLES"; + case AudioApi::AAudio: return "AAudio"; + default: return "Unrecognized audio API"; + } +} + +template<> +const char *convertToText(AudioStream* stream) { + static std::string streamText; + std::stringstream s; + + s<<"StreamID: "<< static_cast(stream)<getDeviceId()<getDirection())<getAudioApi())<getBufferCapacityInFrames()<getBufferSizeInFrames()<getFramesPerBurst()<getFramesPerDataCallback()<getSampleRate()<getChannelCount()<getFormat())<getSharingMode())<getPerformanceMode()) + <getState())<getXRunCount()<getFramesRead()<getFramesWritten()< +const char *convertToText(Usage usage) { + + switch (usage) { + case Usage::Media: return "Media"; + case Usage::VoiceCommunication: return "VoiceCommunication"; + case Usage::VoiceCommunicationSignalling: return "VoiceCommunicationSignalling"; + case Usage::Alarm: return "Alarm"; + case Usage::Notification: return "Notification"; + case Usage::NotificationRingtone: return "NotificationRingtone"; + case Usage::NotificationEvent: return "NotificationEvent"; + case Usage::AssistanceAccessibility: return "AssistanceAccessibility"; + case Usage::AssistanceNavigationGuidance: return "AssistanceNavigationGuidance"; + case Usage::AssistanceSonification: return "AssistanceSonification"; + case Usage::Game: return "Game"; + case Usage::Assistant: return "Assistant"; + default: return "Unrecognized usage"; + } +} + +template<> +const char *convertToText(ContentType contentType) { + + switch (contentType) { + case ContentType::Speech: return "Speech"; + case ContentType::Music: return "Music"; + case ContentType::Movie: return "Movie"; + case ContentType::Sonification: return "Sonification"; + default: return "Unrecognized content type"; + } +} + +template<> +const char *convertToText(InputPreset inputPreset) { + + switch (inputPreset) { + case InputPreset::Generic: return "Generic"; + case InputPreset::Camcorder: return "Camcorder"; + case InputPreset::VoiceRecognition: return "VoiceRecognition"; + case InputPreset::VoiceCommunication: return "VoiceCommunication"; + case InputPreset::Unprocessed: return "Unprocessed"; + case InputPreset::VoicePerformance: return "VoicePerformance"; + default: return "Unrecognized input preset"; + } +} + +template<> +const char *convertToText(SessionId sessionId) { + + switch (sessionId) { + case SessionId::None: return "None"; + case SessionId::Allocate: return "Allocate"; + default: return "Unrecognized session id"; + } +} + +template<> +const char *convertToText(ChannelCount channelCount) { + + switch (channelCount) { + case ChannelCount::Unspecified: return "Unspecified"; + case ChannelCount::Mono: return "Mono"; + case ChannelCount::Stereo: return "Stereo"; + default: return "Unrecognized channel count"; + } +} + +template<> +const char *convertToText(SampleRateConversionQuality sampleRateConversionQuality) { + + switch (sampleRateConversionQuality) { + case SampleRateConversionQuality::None: return "None"; + case SampleRateConversionQuality::Fastest: return "Fastest"; + case SampleRateConversionQuality::Low: return "Low"; + case SampleRateConversionQuality::Medium: return "Medium"; + case SampleRateConversionQuality::High: return "High"; + case SampleRateConversionQuality::Best: return "Best"; + default: return "Unrecognized sample rate conversion quality"; + } +} + +template<> +const char *convertToText(FallbackMode fallbackMode) { + switch (fallbackMode) { + case FallbackMode::Default: return "Default"; + case FallbackMode::Fail: return "Fail"; + case FallbackMode::Mute: return "Mute"; + default: return "Unrecognized fallback mode"; + } +} + +template<> +const char *convertToText(StretchMode stretchMode) { + switch (stretchMode) { + case StretchMode::Default: return "Default"; + case StretchMode::Voice: return "Voice"; + default: return "Unrecognized stretch mode"; + } +} + +std::string getPropertyString(const char * name) { + std::string result; +#ifdef __ANDROID__ + char valueText[PROP_VALUE_MAX] = {0}; + if (__system_property_get(name, valueText) != 0) { + result = valueText; + } +#else + (void) name; +#endif + return result; +} + +int getPropertyInteger(const char * name, int defaultValue) { + int result = defaultValue; +#ifdef __ANDROID__ + char valueText[PROP_VALUE_MAX] = {0}; + if (__system_property_get(name, valueText) != 0) { + result = atoi(valueText); + } +#else + (void) name; +#endif + return result; +} + +int getSdkVersion() { + static int sCachedSdkVersion = -1; +#ifdef __ANDROID__ + if (sCachedSdkVersion == -1) { + sCachedSdkVersion = getPropertyInteger("ro.build.version.sdk", -1); + } +#endif + return sCachedSdkVersion; +} + +bool isAtLeastPreReleaseCodename(const std::string& codename) { + std::string buildCodename = getPropertyString("ro.build.version.codename"); + // Special case "REL", which means the build is not a pre-release build. + if ("REL" == buildCodename) { + return false; + } + + // Otherwise lexically compare them. Return true if the build codename is equal to or + // greater than the requested codename. + return buildCodename.compare(codename) >= 0; +} + +int getChannelCountFromChannelMask(ChannelMask channelMask) { + return __builtin_popcount(static_cast(channelMask)); +} + + +std::set COMPRESSED_FORMATS = { + AudioFormat::MP3, AudioFormat::AAC_LC, AudioFormat::AAC_HE_V1, AudioFormat::AAC_HE_V2, + AudioFormat::AAC_ELD, AudioFormat::AAC_XHE, AudioFormat::OPUS +}; +bool isCompressedFormat(AudioFormat format) { + return COMPRESSED_FORMATS.count(format) != 0; +} + +std::string toString(const PlaybackParameters& parameters) { + std::stringstream ss; + ss << "Fallback Mode: " << convertToText(parameters.fallbackMode) + << ", Stretch Mode:" << convertToText(parameters.stretchMode) + << ", pitch: " << parameters.pitch + << ", speed: " << parameters.speed; + return ss.str(); +} + +}// namespace oboe diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_Version_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_Version_android.cpp new file mode 100644 index 0000000..a9b2ff3 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_common_Version_android.cpp @@ -0,0 +1,28 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "oboe_oboe_Version_android.h" + +namespace oboe { + + // This variable enables the version information to be read from the resulting binary e.g. + // by running `objdump -s --section=.data ` + // Please do not optimize or change in any way. + char kVersionText[] = "OboeVersion" OBOE_VERSION_TEXT; + + const char * getVersionText(){ + return kVersionText; + } +} // namespace oboe diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_fifo_FifoBuffer_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_fifo_FifoBuffer_android.cpp new file mode 100644 index 0000000..f1917e8 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_fifo_FifoBuffer_android.cpp @@ -0,0 +1,178 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include "oboe_oboe_FifoControllerBase_android.h" +#include "oboe_fifo_FifoController_android.h" +#include "oboe_fifo_FifoControllerIndirect_android.h" +#include "oboe_oboe_FifoBuffer_android.h" + +namespace oboe { + +FifoBuffer::FifoBuffer(uint32_t bytesPerFrame, uint32_t capacityInFrames) + : mBytesPerFrame(bytesPerFrame) + , mStorage(nullptr) + , mFramesReadCount(0) + , mFramesUnderrunCount(0) +{ + mFifo = std::make_unique(capacityInFrames); + // allocate buffer + int32_t bytesPerBuffer = bytesPerFrame * capacityInFrames; + mStorage = new uint8_t[bytesPerBuffer]; + mStorageOwned = true; +} + +FifoBuffer::FifoBuffer( uint32_t bytesPerFrame, + uint32_t capacityInFrames, + std::atomic *readCounterAddress, + std::atomic *writeCounterAddress, + uint8_t *dataStorageAddress + ) + : mBytesPerFrame(bytesPerFrame) + , mStorage(dataStorageAddress) + , mFramesReadCount(0) + , mFramesUnderrunCount(0) +{ + mFifo = std::make_unique(capacityInFrames, + readCounterAddress, + writeCounterAddress); + mStorage = dataStorageAddress; + mStorageOwned = false; +} + +FifoBuffer::~FifoBuffer() { + if (mStorageOwned) { + delete[] mStorage; + } +} + +int32_t FifoBuffer::convertFramesToBytes(int32_t frames) { + return frames * mBytesPerFrame; +} + +int32_t FifoBuffer::read(void *buffer, int32_t numFrames) { + if (numFrames <= 0) { + return 0; + } + // safe because numFrames is guaranteed positive + uint32_t framesToRead = static_cast(numFrames); + uint32_t framesAvailable = mFifo->getFullFramesAvailable(); + framesToRead = std::min(framesToRead, framesAvailable); + + uint32_t readIndex = mFifo->getReadIndex(); // ranges 0 to capacity + uint8_t *destination = reinterpret_cast(buffer); + uint8_t *source = &mStorage[convertFramesToBytes(readIndex)]; + if ((readIndex + framesToRead) > mFifo->getFrameCapacity()) { + // read in two parts, first part here is at the end of the mStorage buffer + int32_t frames1 = static_cast(mFifo->getFrameCapacity() - readIndex); + int32_t numBytes = convertFramesToBytes(frames1); + if (numBytes < 0) { + return static_cast(Result::ErrorOutOfRange); + } + memcpy(destination, source, static_cast(numBytes)); + destination += numBytes; + // read second part, which is at the beginning of mStorage + source = &mStorage[0]; + int32_t frames2 = static_cast(framesToRead - frames1); + numBytes = convertFramesToBytes(frames2); + if (numBytes < 0) { + return static_cast(Result::ErrorOutOfRange); + } + memcpy(destination, source, static_cast(numBytes)); + } else { + // just read in one shot + int32_t numBytes = convertFramesToBytes(framesToRead); + if (numBytes < 0) { + return static_cast(Result::ErrorOutOfRange); + } + memcpy(destination, source, static_cast(numBytes)); + } + mFifo->advanceReadIndex(framesToRead); + + return framesToRead; +} + +int32_t FifoBuffer::write(const void *buffer, int32_t numFrames) { + if (numFrames <= 0) { + return 0; + } + // Guaranteed positive. + uint32_t framesToWrite = static_cast(numFrames); + uint32_t framesAvailable = mFifo->getEmptyFramesAvailable(); + framesToWrite = std::min(framesToWrite, framesAvailable); + + uint32_t writeIndex = mFifo->getWriteIndex(); + int byteIndex = convertFramesToBytes(writeIndex); + const uint8_t *source = reinterpret_cast(buffer); + uint8_t *destination = &mStorage[byteIndex]; + if ((writeIndex + framesToWrite) > mFifo->getFrameCapacity()) { + // write in two parts, first part here + int32_t frames1 = static_cast(mFifo->getFrameCapacity() - writeIndex); + int32_t numBytes = convertFramesToBytes(frames1); + if (numBytes < 0) { + return static_cast(Result::ErrorOutOfRange); + } + memcpy(destination, source, static_cast(numBytes)); + // read second part + source += convertFramesToBytes(frames1); + destination = &mStorage[0]; + int frames2 = static_cast(framesToWrite - frames1); + numBytes = convertFramesToBytes(frames2); + if (numBytes < 0) { + return static_cast(Result::ErrorOutOfRange); + } + memcpy(destination, source, static_cast(numBytes)); + } else { + // just write in one shot + int32_t numBytes = convertFramesToBytes(framesToWrite); + if (numBytes < 0) { + return static_cast(Result::ErrorOutOfRange); + } + memcpy(destination, source, static_cast(numBytes)); + } + mFifo->advanceWriteIndex(framesToWrite); + + return framesToWrite; +} + +int32_t FifoBuffer::readNow(void *buffer, int32_t numFrames) { + int32_t framesRead = read(buffer, numFrames); + if (framesRead < 0) { + return framesRead; + } + int32_t framesLeft = numFrames - framesRead; + mFramesReadCount += framesRead; + mFramesUnderrunCount += framesLeft; + // Zero out any samples we could not set. + if (framesLeft > 0) { + uint8_t *destination = reinterpret_cast(buffer); + destination += convertFramesToBytes(framesRead); // point to first byte not set + int32_t bytesToZero = convertFramesToBytes(framesLeft); + memset(destination, 0, static_cast(bytesToZero)); + } + + return framesRead; +} + + +uint32_t FifoBuffer::getBufferCapacityInFrames() const { + return mFifo->getFrameCapacity(); +} + +} // namespace oboe diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_fifo_FifoControllerBase_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_fifo_FifoControllerBase_android.cpp new file mode 100644 index 0000000..b6a4601 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_fifo_FifoControllerBase_android.cpp @@ -0,0 +1,68 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include "oboe_oboe_FifoControllerBase_android.h" + +namespace oboe { + +FifoControllerBase::FifoControllerBase(uint32_t capacityInFrames) + : mTotalFrames(capacityInFrames) +{ + // Avoid ridiculously large buffers and the arithmetic wraparound issues that can follow. + assert(capacityInFrames <= (UINT32_MAX / 4)); +} + +uint32_t FifoControllerBase::getFullFramesAvailable() const { + uint64_t writeCounter = getWriteCounter(); + uint64_t readCounter = getReadCounter(); + if (readCounter > writeCounter) { + return 0; + } + uint64_t delta = writeCounter - readCounter; + if (delta >= mTotalFrames) { + return mTotalFrames; + } + // delta is now guaranteed to fit within the range of a uint32_t + return static_cast(delta); +} + +uint32_t FifoControllerBase::getReadIndex() const { + // % works with non-power of two sizes + return static_cast(getReadCounter() % mTotalFrames); +} + +void FifoControllerBase::advanceReadIndex(uint32_t numFrames) { + incrementReadCounter(numFrames); +} + +uint32_t FifoControllerBase::getEmptyFramesAvailable() const { + return static_cast(mTotalFrames - getFullFramesAvailable()); +} + +uint32_t FifoControllerBase::getWriteIndex() const { + // % works with non-power of two sizes + return static_cast(getWriteCounter() % mTotalFrames); +} + +void FifoControllerBase::advanceWriteIndex(uint32_t numFrames) { + incrementWriteCounter(numFrames); +} + +} // namespace oboe diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_fifo_FifoControllerIndirect_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_fifo_FifoControllerIndirect_android.cpp new file mode 100644 index 0000000..42a4057 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_fifo_FifoControllerIndirect_android.cpp @@ -0,0 +1,32 @@ +/* + * Copyright 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "oboe_fifo_FifoControllerIndirect_android.h" + +namespace oboe { + +FifoControllerIndirect::FifoControllerIndirect(uint32_t numFrames, + std::atomic *readCounterAddress, + std::atomic *writeCounterAddress) + : FifoControllerBase(numFrames) + , mReadCounterAddress(readCounterAddress) + , mWriteCounterAddress(writeCounterAddress) +{ +} + +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_fifo_FifoControllerIndirect_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_fifo_FifoControllerIndirect_android.h new file mode 100644 index 0000000..449989e --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_fifo_FifoControllerIndirect_android.h @@ -0,0 +1,66 @@ +/* + * Copyright 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef NATIVEOBOE_FIFOCONTROLLERINDIRECT_H +#define NATIVEOBOE_FIFOCONTROLLERINDIRECT_H + +#include +#include + +#include "oboe_oboe_FifoControllerBase_android.h" + +namespace oboe { + +/** + * A FifoControllerBase with counters external to the class. + */ +class FifoControllerIndirect : public FifoControllerBase { + +public: + FifoControllerIndirect(uint32_t bufferSize, + std::atomic *readCounterAddress, + std::atomic *writeCounterAddress); + virtual ~FifoControllerIndirect() = default; + + virtual uint64_t getReadCounter() const override { + return mReadCounterAddress->load(std::memory_order_acquire); + } + virtual void setReadCounter(uint64_t n) override { + mReadCounterAddress->store(n, std::memory_order_release); + } + virtual void incrementReadCounter(uint64_t n) override { + mReadCounterAddress->fetch_add(n, std::memory_order_acq_rel); + } + virtual uint64_t getWriteCounter() const override { + return mWriteCounterAddress->load(std::memory_order_acquire); + } + virtual void setWriteCounter(uint64_t n) override { + mWriteCounterAddress->store(n, std::memory_order_release); + } + virtual void incrementWriteCounter(uint64_t n) override { + mWriteCounterAddress->fetch_add(n, std::memory_order_acq_rel); + } + +private: + + std::atomic *mReadCounterAddress; + std::atomic *mWriteCounterAddress; + +}; + +} // namespace oboe + +#endif //NATIVEOBOE_FIFOCONTROLLERINDIRECT_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_fifo_FifoController_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_fifo_FifoController_android.cpp new file mode 100644 index 0000000..ae46b53 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_fifo_FifoController_android.cpp @@ -0,0 +1,30 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "oboe_fifo_FifoController_android.h" + +namespace oboe { + +FifoController::FifoController(uint32_t numFrames) + : FifoControllerBase(numFrames) +{ + setReadCounter(0); + setWriteCounter(0); +} + +} // namespace oboe diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_fifo_FifoController_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_fifo_FifoController_android.h new file mode 100644 index 0000000..a040ab4 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_fifo_FifoController_android.h @@ -0,0 +1,62 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef NATIVEOBOE_FIFOCONTROLLER_H +#define NATIVEOBOE_FIFOCONTROLLER_H + +#include +#include + +#include "oboe_oboe_FifoControllerBase_android.h" + +namespace oboe { + +/** + * A FifoControllerBase with counters contained in the class. + */ +class FifoController : public FifoControllerBase +{ +public: + FifoController(uint32_t bufferSize); + virtual ~FifoController() = default; + + virtual uint64_t getReadCounter() const override { + return mReadCounter.load(std::memory_order_acquire); + } + virtual void setReadCounter(uint64_t n) override { + mReadCounter.store(n, std::memory_order_release); + } + virtual void incrementReadCounter(uint64_t n) override { + mReadCounter.fetch_add(n, std::memory_order_acq_rel); + } + virtual uint64_t getWriteCounter() const override { + return mWriteCounter.load(std::memory_order_acquire); + } + virtual void setWriteCounter(uint64_t n) override { + mWriteCounter.store(n, std::memory_order_release); + } + virtual void incrementWriteCounter(uint64_t n) override { + mWriteCounter.fetch_add(n, std::memory_order_acq_rel); + } + +private: + std::atomic mReadCounter{}; + std::atomic mWriteCounter{}; +}; + +} // namespace oboe + +#endif //NATIVEOBOE_FIFOCONTROLLER_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_ChannelCountConverter_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_ChannelCountConverter_android.cpp new file mode 100644 index 0000000..91eae22 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_ChannelCountConverter_android.cpp @@ -0,0 +1,52 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "oboe_flowgraph_FlowGraphNode_android.h" +#include "oboe_flowgraph_ChannelCountConverter_android.h" + +using namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph; + +ChannelCountConverter::ChannelCountConverter( + int32_t inputChannelCount, + int32_t outputChannelCount) + : input(*this, inputChannelCount) + , output(*this, outputChannelCount) { +} + +ChannelCountConverter::~ChannelCountConverter() = default; + +int32_t ChannelCountConverter::onProcess(int32_t numFrames) { + const float *inputBuffer = input.getBuffer(); + float *outputBuffer = output.getBuffer(); + int32_t inputChannelCount = input.getSamplesPerFrame(); + int32_t outputChannelCount = output.getSamplesPerFrame(); + for (int i = 0; i < numFrames; i++) { + int inputChannel = 0; + for (int outputChannel = 0; outputChannel < outputChannelCount; outputChannel++) { + // Copy input channels to output channels. + // Wrap if we run out of inputs. + // Discard if we run out of outputs. + outputBuffer[outputChannel] = inputBuffer[inputChannel]; + inputChannel = (inputChannel == inputChannelCount) + ? 0 : inputChannel + 1; + } + inputBuffer += inputChannelCount; + outputBuffer += outputChannelCount; + } + return numFrames; +} + diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_ChannelCountConverter_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_ChannelCountConverter_android.h new file mode 100644 index 0000000..dec8cf1 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_ChannelCountConverter_android.h @@ -0,0 +1,52 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FLOWGRAPH_CHANNEL_COUNT_CONVERTER_H +#define FLOWGRAPH_CHANNEL_COUNT_CONVERTER_H + +#include +#include + +#include "oboe_flowgraph_FlowGraphNode_android.h" + +namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph { + +/** + * Change the number of number of channels without mixing. + * When increasing the channel count, duplicate input channels. + * When decreasing the channel count, drop input channels. + */ + class ChannelCountConverter : public FlowGraphNode { + public: + explicit ChannelCountConverter( + int32_t inputChannelCount, + int32_t outputChannelCount); + + virtual ~ChannelCountConverter(); + + int32_t onProcess(int32_t numFrames) override; + + const char *getName() override { + return "ChannelCountConverter"; + } + + FlowGraphPortFloatInput input; + FlowGraphPortFloatOutput output; + }; + +} /* namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph */ + +#endif //FLOWGRAPH_CHANNEL_COUNT_CONVERTER_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_ClipToRange_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_ClipToRange_android.cpp new file mode 100644 index 0000000..d4a0731 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_ClipToRange_android.cpp @@ -0,0 +1,38 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include "oboe_flowgraph_FlowGraphNode_android.h" +#include "oboe_flowgraph_ClipToRange_android.h" + +using namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph; + +ClipToRange::ClipToRange(int32_t channelCount) + : FlowGraphFilter(channelCount) { +} + +int32_t ClipToRange::onProcess(int32_t numFrames) { + const float *inputBuffer = input.getBuffer(); + float *outputBuffer = output.getBuffer(); + + int32_t numSamples = numFrames * output.getSamplesPerFrame(); + for (int32_t i = 0; i < numSamples; i++) { + *outputBuffer++ = std::min(mMaximum, std::max(mMinimum, *inputBuffer++)); + } + + return numFrames; +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_ClipToRange_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_ClipToRange_android.h new file mode 100644 index 0000000..eeadd49 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_ClipToRange_android.h @@ -0,0 +1,68 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FLOWGRAPH_CLIP_TO_RANGE_H +#define FLOWGRAPH_CLIP_TO_RANGE_H + +#include +#include +#include + +#include "oboe_flowgraph_FlowGraphNode_android.h" + +namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph { + +// This is 3 dB, (10^(3/20)), to match the maximum headroom in AudioTrack for float data. +// It is designed to allow occasional transient peaks. +constexpr float kDefaultMaxHeadroom = 1.41253754f; +constexpr float kDefaultMinHeadroom = -kDefaultMaxHeadroom; + +class ClipToRange : public FlowGraphFilter { +public: + explicit ClipToRange(int32_t channelCount); + + virtual ~ClipToRange() = default; + + int32_t onProcess(int32_t numFrames) override; + + void setMinimum(float min) { + mMinimum = min; + } + + float getMinimum() const { + return mMinimum; + } + + void setMaximum(float min) { + mMaximum = min; + } + + float getMaximum() const { + return mMaximum; + } + + const char *getName() override { + return "ClipToRange"; + } + +private: + float mMinimum = kDefaultMinHeadroom; + float mMaximum = kDefaultMaxHeadroom; +}; + +} /* namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph */ + +#endif //FLOWGRAPH_CLIP_TO_RANGE_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_FlowGraphNode_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_FlowGraphNode_android.cpp new file mode 100644 index 0000000..48b739e --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_FlowGraphNode_android.cpp @@ -0,0 +1,114 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "stdio.h" +#include +#include +#include "oboe_flowgraph_FlowGraphNode_android.h" + +using namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph; + +/***************************************************************************/ +int32_t FlowGraphNode::pullData(int32_t numFrames, int64_t callCount) { + int32_t frameCount = numFrames; + // Prevent recursion and multiple execution of nodes. + if (callCount > mLastCallCount) { + mLastCallCount = callCount; + if (mDataPulledAutomatically) { + // Pull from all the upstream nodes. + for (auto &port : mInputPorts) { + // TODO fix bug of leaving unused data in some ports if using multiple AudioSource + frameCount = port.get().pullData(callCount, frameCount); + } + } + if (frameCount > 0) { + frameCount = onProcess(frameCount); + } + mLastFrameCount = frameCount; + } else { + frameCount = mLastFrameCount; + } + return frameCount; +} + +void FlowGraphNode::pullReset() { + if (!mBlockRecursion) { + mBlockRecursion = true; // for cyclic graphs + // Pull reset from all the upstream nodes. + for (auto &port : mInputPorts) { + port.get().pullReset(); + } + mBlockRecursion = false; + reset(); + } +} + +void FlowGraphNode::reset() { + mLastFrameCount = 0; + mLastCallCount = kInitialCallCount; +} + +/***************************************************************************/ +FlowGraphPortFloat::FlowGraphPortFloat(FlowGraphNode &parent, + int32_t samplesPerFrame, + int32_t framesPerBuffer) + : FlowGraphPort(parent, samplesPerFrame) + , mFramesPerBuffer(framesPerBuffer) + , mBuffer(nullptr) { + size_t numFloats = static_cast(framesPerBuffer) * getSamplesPerFrame(); + mBuffer = std::make_unique(numFloats); +} + +/***************************************************************************/ +int32_t FlowGraphPortFloatOutput::pullData(int64_t callCount, int32_t numFrames) { + numFrames = std::min(getFramesPerBuffer(), numFrames); + return mContainingNode.pullData(numFrames, callCount); +} + +void FlowGraphPortFloatOutput::pullReset() { + mContainingNode.pullReset(); +} + +// These need to be in the .cpp file because of forward cross references. +void FlowGraphPortFloatOutput::connect(FlowGraphPortFloatInput *port) { + port->connect(this); +} + +void FlowGraphPortFloatOutput::disconnect(FlowGraphPortFloatInput *port) { + port->disconnect(this); +} + +/***************************************************************************/ +int32_t FlowGraphPortFloatInput::pullData(int64_t callCount, int32_t numFrames) { + return (mConnected == nullptr) + ? std::min(getFramesPerBuffer(), numFrames) + : mConnected->pullData(callCount, numFrames); +} +void FlowGraphPortFloatInput::pullReset() { + if (mConnected != nullptr) mConnected->pullReset(); +} + +float *FlowGraphPortFloatInput::getBuffer() { + if (mConnected == nullptr) { + return FlowGraphPortFloat::getBuffer(); // loaded using setValue() + } else { + return mConnected->getBuffer(); + } +} + +int32_t FlowGraphSink::pullData(int32_t numFrames) { + return FlowGraphNode::pullData(numFrames, getLastCallCount() + 1); +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_FlowGraphNode_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_FlowGraphNode_android.h new file mode 100644 index 0000000..2884c08 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_FlowGraphNode_android.h @@ -0,0 +1,450 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * FlowGraph.h + * + * Processing node and ports that can be used in a simple data flow graph. + * This was designed to work with audio but could be used for other + * types of data. + */ + +#ifndef FLOWGRAPH_FLOW_GRAPH_NODE_H +#define FLOWGRAPH_FLOW_GRAPH_NODE_H + +#include +#include +#include +#include +#include +#include +#include +#include + +// TODO Move these classes into separate files. +// TODO Review use of raw pointers for connect(). Maybe use smart pointers but need to avoid +// run-time deallocation in audio thread. + +// Set flags FLOWGRAPH_ANDROID_INTERNAL and FLOWGRAPH_OUTER_NAMESPACE based on whether compiler +// flag __ANDROID_NDK__ is defined. __ANDROID_NDK__ should be defined in oboe and not aaudio. + +#ifndef FLOWGRAPH_ANDROID_INTERNAL +#ifdef __ANDROID_NDK__ +#define FLOWGRAPH_ANDROID_INTERNAL 0 +#else +#define FLOWGRAPH_ANDROID_INTERNAL 1 +#endif // __ANDROID_NDK__ +#endif // FLOWGRAPH_ANDROID_INTERNAL + +#ifndef FLOWGRAPH_OUTER_NAMESPACE +#ifdef __ANDROID_NDK__ +#define FLOWGRAPH_OUTER_NAMESPACE oboe +#else +#define FLOWGRAPH_OUTER_NAMESPACE aaudio +#endif // __ANDROID_NDK__ +#endif // FLOWGRAPH_OUTER_NAMESPACE + +namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph { + +// Default block size that can be overridden when the FlowGraphPortFloat is created. +// If it is too small then we will have too much overhead from switching between nodes. +// If it is too high then we will thrash the caches. +constexpr int kDefaultBufferSize = 8; // arbitrary + +class FlowGraphPort; +class FlowGraphPortFloatInput; + +/***************************************************************************/ +/** + * Base class for all nodes in the flowgraph. + */ +class FlowGraphNode { +public: + FlowGraphNode() = default; + virtual ~FlowGraphNode() = default; + + /** + * Read from the input ports, + * generate multiple frames of data then write the results to the output ports. + * + * @param numFrames maximum number of frames requested for processing + * @return number of frames actually processed + */ + virtual int32_t onProcess(int32_t numFrames) = 0; + + /** + * If the callCount is at or after the previous callCount then call + * pullData on all of the upstreamNodes. + * Then call onProcess(). + * This prevents infinite recursion in case of cyclic graphs. + * It also prevents nodes upstream from a branch from being executed twice. + * + * @param callCount + * @param numFrames + * @return number of frames valid + */ + int32_t pullData(int32_t numFrames, int64_t callCount); + + /** + * Recursively reset all the nodes in the graph, starting from a Sink. + * + * This must not be called at the same time as pullData! + */ + void pullReset(); + + /** + * Reset framePosition counters. + */ + virtual void reset(); + + void addInputPort(FlowGraphPort &port) { + mInputPorts.emplace_back(port); + } + + bool isDataPulledAutomatically() const { + return mDataPulledAutomatically; + } + + /** + * Set true if you want the data pulled through the graph automatically. + * This is the default. + * + * Set false if you want to pull the data from the input ports in the onProcess() method. + * You might do this, for example, in a sample rate converting node. + * + * @param automatic + */ + void setDataPulledAutomatically(bool automatic) { + mDataPulledAutomatically = automatic; + } + + virtual const char *getName() { + return "FlowGraph"; + } + + int64_t getLastCallCount() { + return mLastCallCount; + } + +protected: + + static constexpr int64_t kInitialCallCount = -1; + int64_t mLastCallCount = kInitialCallCount; + + std::vector> mInputPorts; + +private: + bool mDataPulledAutomatically = true; + bool mBlockRecursion = false; + int32_t mLastFrameCount = 0; + +}; + +/***************************************************************************/ +/** + * This is a connector that allows data to flow between modules. + * + * The ports are the primary means of interacting with a module. + * So they are generally declared as public. + * + */ +class FlowGraphPort { +public: + FlowGraphPort(FlowGraphNode &parent, int32_t samplesPerFrame) + : mContainingNode(parent) + , mSamplesPerFrame(samplesPerFrame) { + } + + virtual ~FlowGraphPort() = default; + + // Ports are often declared public. So let's make them non-copyable. + FlowGraphPort(const FlowGraphPort&) = delete; + FlowGraphPort& operator=(const FlowGraphPort&) = delete; + + int32_t getSamplesPerFrame() const { + return mSamplesPerFrame; + } + + virtual int32_t pullData(int64_t framePosition, int32_t numFrames) = 0; + + virtual void pullReset() {} + +protected: + FlowGraphNode &mContainingNode; + +private: + const int32_t mSamplesPerFrame = 1; +}; + +/***************************************************************************/ +/** + * This port contains a 32-bit float buffer that can contain several frames of data. + * Processing the data in a block improves performance. + * + * The size is framesPerBuffer * samplesPerFrame). + */ +class FlowGraphPortFloat : public FlowGraphPort { +public: + FlowGraphPortFloat(FlowGraphNode &parent, + int32_t samplesPerFrame, + int32_t framesPerBuffer = kDefaultBufferSize + ); + + virtual ~FlowGraphPortFloat() = default; + + int32_t getFramesPerBuffer() const { + return mFramesPerBuffer; + } + +protected: + + /** + * @return buffer internal to the port or from a connected port + */ + virtual float *getBuffer() { + return mBuffer.get(); + } + +private: + const int32_t mFramesPerBuffer = 1; + std::unique_ptr mBuffer; // allocated in constructor +}; + +/***************************************************************************/ +/** + * The results of a node's processing are stored in the buffers of the output ports. + */ +class FlowGraphPortFloatOutput : public FlowGraphPortFloat { +public: + FlowGraphPortFloatOutput(FlowGraphNode &parent, int32_t samplesPerFrame) + : FlowGraphPortFloat(parent, samplesPerFrame) { + } + + virtual ~FlowGraphPortFloatOutput() = default; + + using FlowGraphPortFloat::getBuffer; + + /** + * Connect to the input of another module. + * An input port can only have one connection. + * An output port can have multiple connections. + * If you connect a second output port to an input port + * then it overwrites the previous connection. + * + * This not thread safe. Do not modify the graph topology from another thread while running. + * Also do not delete a module while it is connected to another port if the graph is running. + */ + void connect(FlowGraphPortFloatInput *port); + + /** + * Disconnect from the input of another module. + * This not thread safe. + */ + void disconnect(FlowGraphPortFloatInput *port); + + /** + * Call the parent module's onProcess() method. + * That may pull data from its inputs and recursively + * process the entire graph. + * @return number of frames actually pulled + */ + int32_t pullData(int64_t framePosition, int32_t numFrames) override; + + + void pullReset() override; + +}; + +/***************************************************************************/ + +/** + * An input port for streaming audio data. + * You can set a value that will be used for processing. + * If you connect an output port to this port then its value will be used instead. + */ +class FlowGraphPortFloatInput : public FlowGraphPortFloat { +public: + FlowGraphPortFloatInput(FlowGraphNode &parent, int32_t samplesPerFrame) + : FlowGraphPortFloat(parent, samplesPerFrame) { + // Add to parent so it can pull data from each input. + parent.addInputPort(*this); + } + + virtual ~FlowGraphPortFloatInput() = default; + + /** + * If connected to an output port then this will return + * that output ports buffers. + * If not connected then it returns the input ports own buffer + * which can be loaded using setValue(). + */ + float *getBuffer() override; + + /** + * Write every value of the float buffer. + * This value will be ignored if an output port is connected + * to this port. + */ + void setValue(float value) { + int numFloats = kDefaultBufferSize * getSamplesPerFrame(); + float *buffer = getBuffer(); + for (int i = 0; i < numFloats; i++) { + *buffer++ = value; + } + } + + /** + * Connect to the output of another module. + * An input port can only have one connection. + * An output port can have multiple connections. + * This not thread safe. + */ + void connect(FlowGraphPortFloatOutput *port) { + assert(getSamplesPerFrame() == port->getSamplesPerFrame()); + mConnected = port; + } + + void disconnect(FlowGraphPortFloatOutput *port) { + assert(mConnected == port); + (void) port; + mConnected = nullptr; + } + + void disconnect() { + mConnected = nullptr; + } + + /** + * Pull data from any output port that is connected. + */ + int32_t pullData(int64_t framePosition, int32_t numFrames) override; + + void pullReset() override; + +private: + FlowGraphPortFloatOutput *mConnected = nullptr; +}; + +/***************************************************************************/ + +/** + * Base class for an edge node in a graph that has no upstream nodes. + * It outputs data but does not consume data. + * By default, it will read its data from an external buffer. + */ +class FlowGraphSource : public FlowGraphNode { +public: + explicit FlowGraphSource(int32_t channelCount) + : output(*this, channelCount) { + } + + virtual ~FlowGraphSource() = default; + + FlowGraphPortFloatOutput output; +}; + +/***************************************************************************/ + +/** + * Base class for an edge node in a graph that has no upstream nodes. + * It outputs data but does not consume data. + * By default, it will read its data from an external buffer. + */ +class FlowGraphSourceBuffered : public FlowGraphSource { +public: + explicit FlowGraphSourceBuffered(int32_t channelCount) + : FlowGraphSource(channelCount) {} + + virtual ~FlowGraphSourceBuffered() = default; + + /** + * Specify buffer that the node will read from. + * + * @param data TODO Consider using std::shared_ptr. + * @param numFrames + */ + void setData(const void *data, int32_t numFrames) { + mData = data; + mSizeInFrames = numFrames; + mFrameIndex = 0; + } + +protected: + const void *mData = nullptr; + int32_t mSizeInFrames = 0; // number of frames in mData + int32_t mFrameIndex = 0; // index of next frame to be processed +}; + +/***************************************************************************/ +/** + * Base class for an edge node in a graph that has no downstream nodes. + * It consumes data but does not output data. + * This graph will be executed when data is read() from this node + * by pulling data from upstream nodes. + */ +class FlowGraphSink : public FlowGraphNode { +public: + explicit FlowGraphSink(int32_t channelCount) + : input(*this, channelCount) { + } + + virtual ~FlowGraphSink() = default; + + FlowGraphPortFloatInput input; + + /** + * Do nothing. The work happens in the read() method. + * + * @param numFrames + * @return number of frames actually processed + */ + int32_t onProcess(int32_t numFrames) override { + return numFrames; + } + + virtual int32_t read(void *data, int32_t numFrames) = 0; + +protected: + /** + * Pull data through the graph using this nodes last callCount. + * @param numFrames + * @return + */ + int32_t pullData(int32_t numFrames); +}; + +/***************************************************************************/ +/** + * Base class for a node that has an input and an output with the same number of channels. + * This may include traditional filters, eg. FIR, but also include + * any processing node that converts input to output. + */ +class FlowGraphFilter : public FlowGraphNode { +public: + explicit FlowGraphFilter(int32_t channelCount) + : input(*this, channelCount) + , output(*this, channelCount) { + } + + virtual ~FlowGraphFilter() = default; + + FlowGraphPortFloatInput input; + FlowGraphPortFloatOutput output; +}; + +} /* namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph */ + +#endif /* FLOWGRAPH_FLOW_GRAPH_NODE_H */ diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_FlowgraphUtilities_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_FlowgraphUtilities_android.h new file mode 100644 index 0000000..e277d6e --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_FlowgraphUtilities_android.h @@ -0,0 +1,70 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FLOWGRAPH_UTILITIES_H +#define FLOWGRAPH_UTILITIES_H + +#include +#include + +using namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph; + +class FlowgraphUtilities { +public: +// This was copied from audio_utils/primitives.h +/** + * Convert a single-precision floating point value to a Q0.31 integer value. + * Rounds to nearest, ties away from 0. + * + * Values outside the range [-1.0, 1.0) are properly clamped to -2147483648 and 2147483647, + * including -Inf and +Inf. NaN values are considered undefined, and behavior may change + * depending on hardware and future implementation of this function. + */ +static int32_t clamp32FromFloat(float f) +{ + static const float scale = (float)(1UL << 31); + static const float limpos = 1.; + static const float limneg = -1.; + + if (f <= limneg) { + return INT32_MIN; + } else if (f >= limpos) { + return INT32_MAX; + } + f *= scale; + /* integer conversion is through truncation (though int to float is not). + * ensure that we round to nearest, ties away from 0. + */ + return f > 0 ? f + 0.5 : f - 0.5; +} + +/** + * Convert a single-precision floating point value to a Q0.23 integer value, stored in a + * 32 bit signed integer (technically stored as Q8.23, but clamped to Q0.23). + * + * Values outside the range [-1.0, 1.0) are properly clamped to -8388608 and 8388607, + * including -Inf and +Inf. NaN values are considered undefined, and behavior may change + * depending on hardware and future implementation of this function. + */ +static int32_t clamp24FromFloat(float f) +{ + static const float scale = 1 << 23; + return (int32_t) lroundf(fmaxf(fminf(f * scale, scale - 1.f), -scale)); +} + +}; + +#endif // FLOWGRAPH_UTILITIES_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_Limiter_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_Limiter_android.cpp new file mode 100644 index 0000000..cc33ebe --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_Limiter_android.cpp @@ -0,0 +1,67 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include "oboe_flowgraph_FlowGraphNode_android.h" +#include "oboe_flowgraph_Limiter_android.h" + +using namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph; + +Limiter::Limiter(int32_t channelCount) + : FlowGraphFilter(channelCount) { +} + +int32_t Limiter::onProcess(int32_t numFrames) { + const float *inputBuffer = input.getBuffer(); + float *outputBuffer = output.getBuffer(); + + int32_t numSamples = numFrames * output.getSamplesPerFrame(); + + // Cache the last valid output to reduce memory read/write + float lastValidOutput = mLastValidOutput; + + for (int32_t i = 0; i < numSamples; i++) { + // Use the previous output if the input is NaN + if (!isnan(*inputBuffer)) { + lastValidOutput = processFloat(*inputBuffer); + } + inputBuffer++; + *outputBuffer++ = lastValidOutput; + } + mLastValidOutput = lastValidOutput; + + return numFrames; +} + +float Limiter::processFloat(float in) +{ + float in_abs = fabsf(in); + if (in_abs <= 1) { + return in; + } + float out; + if (in_abs < kXWhenYis3Decibels) { + out = (kPolynomialSplineA * in_abs + kPolynomialSplineB) * in_abs + kPolynomialSplineC; + } else { + out = M_SQRT2; + } + if (in < 0) { + out = -out; + } + return out; +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_Limiter_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_Limiter_android.h new file mode 100644 index 0000000..beb1982 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_Limiter_android.h @@ -0,0 +1,64 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FLOWGRAPH_LIMITER_H +#define FLOWGRAPH_LIMITER_H + +#include +#include +#include + +#include "oboe_flowgraph_FlowGraphNode_android.h" + +namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph { + +class Limiter : public FlowGraphFilter { +public: + explicit Limiter(int32_t channelCount); + + int32_t onProcess(int32_t numFrames) override; + + const char *getName() override { + return "Limiter"; + } + +private: + // These numbers are based on a polynomial spline for a quadratic solution Ax^2 + Bx + C + // The range is up to 3 dB, (10^(3/20)), to match AudioTrack for float data. + static constexpr float kPolynomialSplineA = -0.6035533905; // -(1+sqrt(2))/4 + static constexpr float kPolynomialSplineB = 2.2071067811; // (3+sqrt(2))/2 + static constexpr float kPolynomialSplineC = -0.6035533905; // -(1+sqrt(2))/4 + static constexpr float kXWhenYis3Decibels = 1.8284271247; // -1+2sqrt(2) + + /** + * Process an input based on the following: + * If between -1 and 1, return the input value. + * If above kXWhenYis3Decibels, return sqrt(2). + * If below -kXWhenYis3Decibels, return -sqrt(2). + * If between 1 and kXWhenYis3Decibels, use a quadratic spline (Ax^2 + Bx + C). + * If between -kXWhenYis3Decibels and -1, use the absolute value for the spline and flip it. + * The derivative of the spline is 1 at 1 and 0 at kXWhenYis3Decibels. + * This way, the graph is both continuous and differentiable. + */ + float processFloat(float in); + + // Use the previous valid output for NaN inputs + float mLastValidOutput = 0.0f; +}; + +} /* namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph */ + +#endif //FLOWGRAPH_LIMITER_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_ManyToMultiConverter_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_ManyToMultiConverter_android.cpp new file mode 100644 index 0000000..23dcd4f --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_ManyToMultiConverter_android.cpp @@ -0,0 +1,47 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "oboe_flowgraph_ManyToMultiConverter_android.h" + +using namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph; + +ManyToMultiConverter::ManyToMultiConverter(int32_t channelCount) + : inputs(channelCount) + , output(*this, channelCount) { + for (int i = 0; i < channelCount; i++) { + inputs[i] = std::make_unique(*this, 1); + } +} + +int32_t ManyToMultiConverter::onProcess(int32_t numFrames) { + int32_t channelCount = output.getSamplesPerFrame(); + + for (int ch = 0; ch < channelCount; ch++) { + const float *inputBuffer = inputs[ch]->getBuffer(); + float *outputBuffer = output.getBuffer() + ch; + + for (int i = 0; i < numFrames; i++) { + // read one, write into the proper interleaved output channel + float sample = *inputBuffer++; + *outputBuffer = sample; + outputBuffer += channelCount; // advance to next multichannel frame + } + } + return numFrames; +} + diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_ManyToMultiConverter_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_ManyToMultiConverter_android.h new file mode 100644 index 0000000..9199ee7 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_ManyToMultiConverter_android.h @@ -0,0 +1,53 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FLOWGRAPH_MANY_TO_MULTI_CONVERTER_H +#define FLOWGRAPH_MANY_TO_MULTI_CONVERTER_H + +#include +#include +#include + +#include "oboe_flowgraph_FlowGraphNode_android.h" + +namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph { + +/** + * Combine multiple mono inputs into one interleaved multi-channel output. + */ +class ManyToMultiConverter : public flowgraph::FlowGraphNode { +public: + explicit ManyToMultiConverter(int32_t channelCount); + + virtual ~ManyToMultiConverter() = default; + + int32_t onProcess(int numFrames) override; + + void setEnabled(bool /*enabled*/) {} + + std::vector> inputs; + flowgraph::FlowGraphPortFloatOutput output; + + const char *getName() override { + return "ManyToMultiConverter"; + } + +private: +}; + +} /* namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph */ + +#endif //FLOWGRAPH_MANY_TO_MULTI_CONVERTER_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_MonoBlend_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_MonoBlend_android.cpp new file mode 100644 index 0000000..da4bbd0 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_MonoBlend_android.cpp @@ -0,0 +1,46 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "oboe_flowgraph_MonoBlend_android.h" + +using namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph; + +MonoBlend::MonoBlend(int32_t channelCount) + : FlowGraphFilter(channelCount) + , mInvChannelCount(1. / channelCount) +{ +} + +int32_t MonoBlend::onProcess(int32_t numFrames) { + int32_t channelCount = output.getSamplesPerFrame(); + const float *inputBuffer = input.getBuffer(); + float *outputBuffer = output.getBuffer(); + + for (size_t i = 0; i < numFrames; ++i) { + float accum = 0; + for (size_t j = 0; j < channelCount; ++j) { + accum += *inputBuffer++; + } + accum *= mInvChannelCount; + for (size_t j = 0; j < channelCount; ++j) { + *outputBuffer++ = accum; + } + } + + return numFrames; +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_MonoBlend_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_MonoBlend_android.h new file mode 100644 index 0000000..223275e --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_MonoBlend_android.h @@ -0,0 +1,48 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FLOWGRAPH_MONO_BLEND_H +#define FLOWGRAPH_MONO_BLEND_H + +#include +#include + +#include "oboe_flowgraph_FlowGraphNode_android.h" + +namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph { + +/** + * Combine data between multiple channels so each channel is an average + * of all channels. + */ +class MonoBlend : public FlowGraphFilter { +public: + explicit MonoBlend(int32_t channelCount); + + virtual ~MonoBlend() = default; + + int32_t onProcess(int32_t numFrames) override; + + const char *getName() override { + return "MonoBlend"; + } +private: + const float mInvChannelCount; +}; + +} /* namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph */ + +#endif //FLOWGRAPH_MONO_BLEND diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_MonoToMultiConverter_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_MonoToMultiConverter_android.cpp new file mode 100644 index 0000000..b70c780 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_MonoToMultiConverter_android.cpp @@ -0,0 +1,41 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "oboe_flowgraph_FlowGraphNode_android.h" +#include "oboe_flowgraph_MonoToMultiConverter_android.h" + +using namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph; + +MonoToMultiConverter::MonoToMultiConverter(int32_t outputChannelCount) + : input(*this, 1) + , output(*this, outputChannelCount) { +} + +int32_t MonoToMultiConverter::onProcess(int32_t numFrames) { + const float *inputBuffer = input.getBuffer(); + float *outputBuffer = output.getBuffer(); + int32_t channelCount = output.getSamplesPerFrame(); + for (int i = 0; i < numFrames; i++) { + // read one, write many + float sample = *inputBuffer++; + for (int channel = 0; channel < channelCount; channel++) { + *outputBuffer++ = sample; + } + } + return numFrames; +} + diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_MonoToMultiConverter_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_MonoToMultiConverter_android.h new file mode 100644 index 0000000..a7a1453 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_MonoToMultiConverter_android.h @@ -0,0 +1,49 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FLOWGRAPH_MONO_TO_MULTI_CONVERTER_H +#define FLOWGRAPH_MONO_TO_MULTI_CONVERTER_H + +#include +#include + +#include "oboe_flowgraph_FlowGraphNode_android.h" + +namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph { + +/** + * Convert a monophonic stream to a multi-channel interleaved stream + * with the same signal on each channel. + */ +class MonoToMultiConverter : public FlowGraphNode { +public: + explicit MonoToMultiConverter(int32_t outputChannelCount); + + virtual ~MonoToMultiConverter() = default; + + int32_t onProcess(int32_t numFrames) override; + + const char *getName() override { + return "MonoToMultiConverter"; + } + + FlowGraphPortFloatInput input; + FlowGraphPortFloatOutput output; +}; + +} /* namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph */ + +#endif //FLOWGRAPH_MONO_TO_MULTI_CONVERTER_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_MultiToManyConverter_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_MultiToManyConverter_android.cpp new file mode 100644 index 0000000..ab61645 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_MultiToManyConverter_android.cpp @@ -0,0 +1,47 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "oboe_flowgraph_FlowGraphNode_android.h" +#include "oboe_flowgraph_MultiToManyConverter_android.h" + +using namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph; + +MultiToManyConverter::MultiToManyConverter(int32_t channelCount) + : outputs(channelCount) + , input(*this, channelCount) { + for (int i = 0; i < channelCount; i++) { + outputs[i] = std::make_unique(*this, 1); + } +} + +MultiToManyConverter::~MultiToManyConverter() = default; + +int32_t MultiToManyConverter::onProcess(int32_t numFrames) { + int32_t channelCount = input.getSamplesPerFrame(); + + for (int ch = 0; ch < channelCount; ch++) { + const float *inputBuffer = input.getBuffer() + ch; + float *outputBuffer = outputs[ch]->getBuffer(); + + for (int i = 0; i < numFrames; i++) { + *outputBuffer++ = *inputBuffer; + inputBuffer += channelCount; + } + } + + return numFrames; +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_MultiToManyConverter_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_MultiToManyConverter_android.h new file mode 100644 index 0000000..e315b51 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_MultiToManyConverter_android.h @@ -0,0 +1,49 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FLOWGRAPH_MULTI_TO_MANY_CONVERTER_H +#define FLOWGRAPH_MULTI_TO_MANY_CONVERTER_H + +#include +#include + +#include "oboe_flowgraph_FlowGraphNode_android.h" + +namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph { + +/** + * Convert a multi-channel interleaved stream to multiple mono-channel + * outputs + */ + class MultiToManyConverter : public FlowGraphNode { + public: + explicit MultiToManyConverter(int32_t channelCount); + + virtual ~MultiToManyConverter(); + + int32_t onProcess(int32_t numFrames) override; + + const char *getName() override { + return "MultiToManyConverter"; + } + + std::vector> outputs; + flowgraph::FlowGraphPortFloatInput input; + }; + +} /* namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph */ + +#endif //FLOWGRAPH_MULTI_TO_MANY_CONVERTER_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_MultiToMonoConverter_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_MultiToMonoConverter_android.cpp new file mode 100644 index 0000000..3b7888d --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_MultiToMonoConverter_android.cpp @@ -0,0 +1,41 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "oboe_flowgraph_FlowGraphNode_android.h" +#include "oboe_flowgraph_MultiToMonoConverter_android.h" + +using namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph; + +MultiToMonoConverter::MultiToMonoConverter(int32_t inputChannelCount) + : input(*this, inputChannelCount) + , output(*this, 1) { +} + +MultiToMonoConverter::~MultiToMonoConverter() = default; + +int32_t MultiToMonoConverter::onProcess(int32_t numFrames) { + const float *inputBuffer = input.getBuffer(); + float *outputBuffer = output.getBuffer(); + int32_t channelCount = input.getSamplesPerFrame(); + for (int i = 0; i < numFrames; i++) { + // read first channel of multi stream, write many + *outputBuffer++ = *inputBuffer; + inputBuffer += channelCount; + } + return numFrames; +} + diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_MultiToMonoConverter_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_MultiToMonoConverter_android.h new file mode 100644 index 0000000..1d14de7 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_MultiToMonoConverter_android.h @@ -0,0 +1,49 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FLOWGRAPH_MULTI_TO_MONO_CONVERTER_H +#define FLOWGRAPH_MULTI_TO_MONO_CONVERTER_H + +#include +#include + +#include "oboe_flowgraph_FlowGraphNode_android.h" + +namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph { + +/** + * Convert a multi-channel interleaved stream to a monophonic stream + * by extracting channel[0]. + */ + class MultiToMonoConverter : public FlowGraphNode { + public: + explicit MultiToMonoConverter(int32_t inputChannelCount); + + virtual ~MultiToMonoConverter(); + + int32_t onProcess(int32_t numFrames) override; + + const char *getName() override { + return "MultiToMonoConverter"; + } + + FlowGraphPortFloatInput input; + FlowGraphPortFloatOutput output; + }; + +} /* namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph */ + +#endif //FLOWGRAPH_MULTI_TO_MONO_CONVERTER_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_RampLinear_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_RampLinear_android.cpp new file mode 100644 index 0000000..1bb72bf --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_RampLinear_android.cpp @@ -0,0 +1,81 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include "oboe_flowgraph_FlowGraphNode_android.h" +#include "oboe_flowgraph_RampLinear_android.h" + +using namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph; + +RampLinear::RampLinear(int32_t channelCount) + : FlowGraphFilter(channelCount) { + mTarget.store(1.0f); +} + +void RampLinear::setLengthInFrames(int32_t frames) { + mLengthInFrames = frames; +} + +void RampLinear::setTarget(float target) { + mTarget.store(target); + // If the ramp has not been used then start immediately at this level. + if (mLastCallCount == kInitialCallCount) { + forceCurrent(target); + } +} + +float RampLinear::interpolateCurrent() { + return mLevelTo - (mRemaining * mScaler); +} + +int32_t RampLinear::onProcess(int32_t numFrames) { + const float *inputBuffer = input.getBuffer(); + float *outputBuffer = output.getBuffer(); + int32_t channelCount = output.getSamplesPerFrame(); + + float target = getTarget(); + if (target != mLevelTo) { + // Start new ramp. Continue from previous level. + mLevelFrom = interpolateCurrent(); + mLevelTo = target; + mRemaining = mLengthInFrames; + mScaler = (mLevelTo - mLevelFrom) / mLengthInFrames; // for interpolation + } + + int32_t framesLeft = numFrames; + + if (mRemaining > 0) { // Ramping? This doesn't happen very often. + int32_t framesToRamp = std::min(framesLeft, mRemaining); + framesLeft -= framesToRamp; + while (framesToRamp > 0) { + float currentLevel = interpolateCurrent(); + for (int ch = 0; ch < channelCount; ch++) { + *outputBuffer++ = *inputBuffer++ * currentLevel; + } + mRemaining--; + framesToRamp--; + } + } + + // Process any frames after the ramp. + int32_t samplesLeft = framesLeft * channelCount; + for (int i = 0; i < samplesLeft; i++) { + *outputBuffer++ = *inputBuffer++ * mLevelTo; + } + + return numFrames; +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_RampLinear_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_RampLinear_android.h new file mode 100644 index 0000000..2a67199 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_RampLinear_android.h @@ -0,0 +1,96 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FLOWGRAPH_RAMP_LINEAR_H +#define FLOWGRAPH_RAMP_LINEAR_H + +#include +#include +#include + +#include "oboe_flowgraph_FlowGraphNode_android.h" + +namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph { + +/** + * When the target is modified then the output will ramp smoothly + * between the original and the new target value. + * This can be used to smooth out control values and reduce pops. + * + * The target may be updated while a ramp is in progress, which will trigger + * a new ramp from the current value. + */ +class RampLinear : public FlowGraphFilter { +public: + explicit RampLinear(int32_t channelCount); + + virtual ~RampLinear() = default; + + int32_t onProcess(int32_t numFrames) override; + + /** + * This is used for the next ramp. + * Calling this does not affect a ramp that is in progress. + */ + void setLengthInFrames(int32_t frames); + + int32_t getLengthInFrames() const { + return mLengthInFrames; + } + + /** + * This may be safely called by another thread. + * @param target + */ + void setTarget(float target); + + float getTarget() const { + return mTarget.load(); + } + + /** + * Force the nextSegment to start from this level. + * + * WARNING: this can cause a discontinuity if called while the ramp is being used. + * Only call this when setting the initial ramp. + * + * @param level + */ + void forceCurrent(float level) { + mLevelFrom = level; + mLevelTo = level; + } + + const char *getName() override { + return "RampLinear"; + } + +private: + + float interpolateCurrent(); + + std::atomic mTarget; + + int32_t mLengthInFrames = 48000.0f / 100.0f ; // 10 msec at 48000 Hz; + int32_t mRemaining = 0; + float mScaler = 0.0f; + float mLevelFrom = 0.0f; + float mLevelTo = 0.0f; +}; + +} /* namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph */ + +#endif //FLOWGRAPH_RAMP_LINEAR_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SampleRateConverter_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SampleRateConverter_android.cpp new file mode 100644 index 0000000..5b6a11f --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SampleRateConverter_android.cpp @@ -0,0 +1,72 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "oboe_flowgraph_SampleRateConverter_android.h" + +using namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph; +using namespace RESAMPLER_OUTER_NAMESPACE::resampler; + +SampleRateConverter::SampleRateConverter(int32_t channelCount, + MultiChannelResampler &resampler) + : FlowGraphFilter(channelCount) + , mResampler(resampler) { + setDataPulledAutomatically(false); +} + +void SampleRateConverter::reset() { + FlowGraphNode::reset(); + mInputCallCount = kInitialCallCount; + mInputCursor = 0; +} + +// Return true if there is a sample available. +bool SampleRateConverter::isInputAvailable() { + // If we have consumed all of the input data then go out and get some more. + if (mInputCursor >= mNumValidInputFrames) { + mInputCallCount++; + mNumValidInputFrames = input.pullData(mInputCallCount, input.getFramesPerBuffer()); + mInputCursor = 0; + } + return (mInputCursor < mNumValidInputFrames); +} + +const float *SampleRateConverter::getNextInputFrame() { + const float *inputBuffer = input.getBuffer(); + return &inputBuffer[mInputCursor++ * input.getSamplesPerFrame()]; +} + +int32_t SampleRateConverter::onProcess(int32_t numFrames) { + float *outputBuffer = output.getBuffer(); + int32_t channelCount = output.getSamplesPerFrame(); + int framesLeft = numFrames; + while (framesLeft > 0) { + // Gather input samples as needed. + if(mResampler.isWriteNeeded()) { + if (isInputAvailable()) { + const float *frame = getNextInputFrame(); + mResampler.writeNextFrame(frame); + } else { + break; + } + } else { + // Output frame is interpolated from input samples. + mResampler.readNextFrame(outputBuffer); + outputBuffer += channelCount; + framesLeft--; + } + } + return numFrames - framesLeft; +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SampleRateConverter_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SampleRateConverter_android.h new file mode 100644 index 0000000..40a3010 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SampleRateConverter_android.h @@ -0,0 +1,63 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FLOWGRAPH_SAMPLE_RATE_CONVERTER_H +#define FLOWGRAPH_SAMPLE_RATE_CONVERTER_H + +#include +#include + +#include "oboe_flowgraph_FlowGraphNode_android.h" +#include "oboe_flowgraph_resampler_MultiChannelResampler_android.h" + +namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph { + +class SampleRateConverter : public FlowGraphFilter { +public: + explicit SampleRateConverter(int32_t channelCount, + resampler::MultiChannelResampler &mResampler); + + virtual ~SampleRateConverter() = default; + + int32_t onProcess(int32_t numFrames) override; + + const char *getName() override { + return "SampleRateConverter"; + } + + void reset() override; + +private: + + // Return true if there is a sample available. + bool isInputAvailable(); + + // This assumes data is available. Only call after calling isInputAvailable(). + const float *getNextInputFrame(); + + resampler::MultiChannelResampler &mResampler; + + int32_t mInputCursor = 0; // offset into the input port buffer + int32_t mNumValidInputFrames = 0; // number of valid frames currently in the input port buffer + // We need our own callCount for upstream calls because calls occur at a different rate. + // This means we cannot have cyclic graphs or merges that contain an SRC. + int64_t mInputCallCount = kInitialCallCount; + +}; + +} /* namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph */ + +#endif //FLOWGRAPH_SAMPLE_RATE_CONVERTER_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SinkFloat_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SinkFloat_android.cpp new file mode 100644 index 0000000..e89e85f --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SinkFloat_android.cpp @@ -0,0 +1,46 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include "oboe_flowgraph_FlowGraphNode_android.h" +#include "oboe_flowgraph_SinkFloat_android.h" + +using namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph; + +SinkFloat::SinkFloat(int32_t channelCount) + : FlowGraphSink(channelCount) { +} + +int32_t SinkFloat::read(void *data, int32_t numFrames) { + float *floatData = (float *) data; + const int32_t channelCount = input.getSamplesPerFrame(); + + int32_t framesLeft = numFrames; + while (framesLeft > 0) { + // Run the graph and pull data through the input port. + int32_t framesPulled = pullData(framesLeft); + if (framesPulled <= 0) { + break; + } + const float *signal = input.getBuffer(); + int32_t numSamples = framesPulled * channelCount; + memcpy(floatData, signal, numSamples * sizeof(float)); + floatData += numSamples; + framesLeft -= framesPulled; + } + return numFrames - framesLeft; +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SinkFloat_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SinkFloat_android.h new file mode 100644 index 0000000..adbce83 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SinkFloat_android.h @@ -0,0 +1,45 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +#ifndef FLOWGRAPH_SINK_FLOAT_H +#define FLOWGRAPH_SINK_FLOAT_H + +#include +#include + +#include "oboe_flowgraph_FlowGraphNode_android.h" + +namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph { + +/** + * AudioSink that lets you read data as 32-bit floats. + */ +class SinkFloat : public FlowGraphSink { +public: + explicit SinkFloat(int32_t channelCount); + ~SinkFloat() override = default; + + int32_t read(void *data, int32_t numFrames) override; + + const char *getName() override { + return "SinkFloat"; + } +}; + +} /* namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph */ + +#endif //FLOWGRAPH_SINK_FLOAT_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SinkI16_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SinkI16_android.cpp new file mode 100644 index 0000000..41142ad --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SinkI16_android.cpp @@ -0,0 +1,57 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include "oboe_flowgraph_SinkI16_android.h" + +#if FLOWGRAPH_ANDROID_INTERNAL +#include +#endif + +using namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph; + +SinkI16::SinkI16(int32_t channelCount) + : FlowGraphSink(channelCount) {} + +int32_t SinkI16::read(void *data, int32_t numFrames) { + int16_t *shortData = (int16_t *) data; + const int32_t channelCount = input.getSamplesPerFrame(); + + int32_t framesLeft = numFrames; + while (framesLeft > 0) { + // Run the graph and pull data through the input port. + int32_t framesRead = pullData(framesLeft); + if (framesRead <= 0) { + break; + } + const float *signal = input.getBuffer(); + int32_t numSamples = framesRead * channelCount; +#if FLOWGRAPH_ANDROID_INTERNAL + memcpy_to_i16_from_float(shortData, signal, numSamples); + shortData += numSamples; + signal += numSamples; +#else + for (int i = 0; i < numSamples; i++) { + int32_t n = (int32_t) (*signal++ * 32768.0f); + *shortData++ = std::min(INT16_MAX, std::max(INT16_MIN, n)); // clip + } +#endif + framesLeft -= framesRead; + } + return numFrames - framesLeft; +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SinkI16_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SinkI16_android.h new file mode 100644 index 0000000..5ac36a0 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SinkI16_android.h @@ -0,0 +1,43 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FLOWGRAPH_SINK_I16_H +#define FLOWGRAPH_SINK_I16_H + +#include +#include + +#include "oboe_flowgraph_FlowGraphNode_android.h" + +namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph { + +/** + * AudioSink that lets you read data as 16-bit signed integers. + */ +class SinkI16 : public FlowGraphSink { +public: + explicit SinkI16(int32_t channelCount); + + int32_t read(void *data, int32_t numFrames) override; + + const char *getName() override { + return "SinkI16"; + } +}; + +} /* namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph */ + +#endif //FLOWGRAPH_SINK_I16_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SinkI24_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SinkI24_android.cpp new file mode 100644 index 0000000..4926d0d --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SinkI24_android.cpp @@ -0,0 +1,66 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + + +#include "oboe_flowgraph_FlowGraphNode_android.h" +#include "oboe_flowgraph_SinkI24_android.h" + +#if FLOWGRAPH_ANDROID_INTERNAL +#include +#endif + +using namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph; + +SinkI24::SinkI24(int32_t channelCount) + : FlowGraphSink(channelCount) {} + +int32_t SinkI24::read(void *data, int32_t numFrames) { + uint8_t *byteData = (uint8_t *) data; + const int32_t channelCount = input.getSamplesPerFrame(); + + int32_t framesLeft = numFrames; + while (framesLeft > 0) { + // Run the graph and pull data through the input port. + int32_t framesRead = pullData(framesLeft); + if (framesRead <= 0) { + break; + } + const float *floatData = input.getBuffer(); + int32_t numSamples = framesRead * channelCount; +#if FLOWGRAPH_ANDROID_INTERNAL + memcpy_to_p24_from_float(byteData, floatData, numSamples); + static const int kBytesPerI24Packed = 3; + byteData += numSamples * kBytesPerI24Packed; + floatData += numSamples; +#else + const int32_t kI24PackedMax = 0x007FFFFF; + const int32_t kI24PackedMin = 0xFF800000; + for (int i = 0; i < numSamples; i++) { + int32_t n = (int32_t) (*floatData++ * 0x00800000); + n = std::min(kI24PackedMax, std::max(kI24PackedMin, n)); // clip + // Write as a packed 24-bit integer in Little Endian format. + *byteData++ = (uint8_t) n; + *byteData++ = (uint8_t) (n >> 8); + *byteData++ = (uint8_t) (n >> 16); + } +#endif + framesLeft -= framesRead; + } + return numFrames - framesLeft; +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SinkI24_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SinkI24_android.h new file mode 100644 index 0000000..74d1ec9 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SinkI24_android.h @@ -0,0 +1,44 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FLOWGRAPH_SINK_I24_H +#define FLOWGRAPH_SINK_I24_H + +#include +#include + +#include "oboe_flowgraph_FlowGraphNode_android.h" + +namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph { + +/** + * AudioSink that lets you read data as packed 24-bit signed integers. + * The sample size is 3 bytes. + */ +class SinkI24 : public FlowGraphSink { +public: + explicit SinkI24(int32_t channelCount); + + int32_t read(void *data, int32_t numFrames) override; + + const char *getName() override { + return "SinkI24"; + } +}; + +} /* namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph */ + +#endif //FLOWGRAPH_SINK_I24_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SinkI32_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SinkI32_android.cpp new file mode 100644 index 0000000..8f1adcb --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SinkI32_android.cpp @@ -0,0 +1,55 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "oboe_flowgraph_FlowGraphNode_android.h" +#include "oboe_flowgraph_FlowgraphUtilities_android.h" +#include "oboe_flowgraph_SinkI32_android.h" + +#if FLOWGRAPH_ANDROID_INTERNAL +#include +#endif + +using namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph; + +SinkI32::SinkI32(int32_t channelCount) + : FlowGraphSink(channelCount) {} + +int32_t SinkI32::read(void *data, int32_t numFrames) { + int32_t *intData = (int32_t *) data; + const int32_t channelCount = input.getSamplesPerFrame(); + + int32_t framesLeft = numFrames; + while (framesLeft > 0) { + // Run the graph and pull data through the input port. + int32_t framesRead = pullData(framesLeft); + if (framesRead <= 0) { + break; + } + const float *signal = input.getBuffer(); + int32_t numSamples = framesRead * channelCount; +#if FLOWGRAPH_ANDROID_INTERNAL + memcpy_to_i32_from_float(intData, signal, numSamples); + intData += numSamples; + signal += numSamples; +#else + for (int i = 0; i < numSamples; i++) { + *intData++ = FlowgraphUtilities::clamp32FromFloat(*signal++); + } +#endif + framesLeft -= framesRead; + } + return numFrames - framesLeft; +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SinkI32_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SinkI32_android.h new file mode 100644 index 0000000..221692b --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SinkI32_android.h @@ -0,0 +1,40 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FLOWGRAPH_SINK_I32_H +#define FLOWGRAPH_SINK_I32_H + +#include + +#include "oboe_flowgraph_FlowGraphNode_android.h" + +namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph { + +class SinkI32 : public FlowGraphSink { +public: + explicit SinkI32(int32_t channelCount); + ~SinkI32() override = default; + + int32_t read(void *data, int32_t numFrames) override; + + const char *getName() override { + return "SinkI32"; + } +}; + +} /* namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph */ + +#endif //FLOWGRAPH_SINK_I32_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SinkI8_24_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SinkI8_24_android.cpp new file mode 100644 index 0000000..9681983 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SinkI8_24_android.cpp @@ -0,0 +1,55 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "oboe_flowgraph_FlowGraphNode_android.h" +#include "oboe_flowgraph_FlowgraphUtilities_android.h" +#include "oboe_flowgraph_SinkI8_24_android.h" + +#if FLOWGRAPH_ANDROID_INTERNAL +#include +#endif + +using namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph; + +SinkI8_24::SinkI8_24(int32_t channelCount) + : FlowGraphSink(channelCount) {} + +int32_t SinkI8_24::read(void *data, int32_t numFrames) { + int32_t *intData = (int32_t *) data; + const int32_t channelCount = input.getSamplesPerFrame(); + + int32_t framesLeft = numFrames; + while (framesLeft > 0) { + // Run the graph and pull data through the input port. + int32_t framesRead = pullData(framesLeft); + if (framesRead <= 0) { + break; + } + const float *signal = input.getBuffer(); + int32_t numSamples = framesRead * channelCount; +#if FLOWGRAPH_ANDROID_INTERNAL + memcpy_to_q8_23_from_float_with_clamp(intData, signal, numSamples); + intData += numSamples; + signal += numSamples; +#else + for (int i = 0; i < numSamples; i++) { + *intData++ = FlowgraphUtilities::clamp24FromFloat(*signal++); + } +#endif + framesLeft -= framesRead; + } + return numFrames - framesLeft; +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SinkI8_24_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SinkI8_24_android.h new file mode 100644 index 0000000..1dcb272 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SinkI8_24_android.h @@ -0,0 +1,40 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FLOWGRAPH_SINK_I8_24_H +#define FLOWGRAPH_SINK_I8_24_H + +#include + +#include "oboe_flowgraph_FlowGraphNode_android.h" + +namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph { + + class SinkI8_24 : public FlowGraphSink { + public: + explicit SinkI8_24(int32_t channelCount); + ~SinkI8_24() override = default; + + int32_t read(void *data, int32_t numFrames) override; + + const char *getName() override { + return "SinkI8_24"; + } + }; + +} /* namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph */ + +#endif //FLOWGRAPH_SINK_I8_24_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SourceFloat_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SourceFloat_android.cpp new file mode 100644 index 0000000..5780240 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SourceFloat_android.cpp @@ -0,0 +1,42 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include "oboe_flowgraph_FlowGraphNode_android.h" +#include "oboe_flowgraph_SourceFloat_android.h" + +using namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph; + +SourceFloat::SourceFloat(int32_t channelCount) + : FlowGraphSourceBuffered(channelCount) { +} + +int32_t SourceFloat::onProcess(int32_t numFrames) { + float *outputBuffer = output.getBuffer(); + const int32_t channelCount = output.getSamplesPerFrame(); + + const int32_t framesLeft = mSizeInFrames - mFrameIndex; + const int32_t framesToProcess = std::min(numFrames, framesLeft); + const int32_t numSamples = framesToProcess * channelCount; + + const float *floatBase = (float *) mData; + const float *floatData = &floatBase[mFrameIndex * channelCount]; + memcpy(outputBuffer, floatData, numSamples * sizeof(float)); + mFrameIndex += framesToProcess; + return framesToProcess; +} + diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SourceFloat_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SourceFloat_android.h new file mode 100644 index 0000000..461b202 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SourceFloat_android.h @@ -0,0 +1,44 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FLOWGRAPH_SOURCE_FLOAT_H +#define FLOWGRAPH_SOURCE_FLOAT_H + +#include +#include + +#include "oboe_flowgraph_FlowGraphNode_android.h" + +namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph { + +/** + * AudioSource that reads a block of pre-defined float data. + */ +class SourceFloat : public FlowGraphSourceBuffered { +public: + explicit SourceFloat(int32_t channelCount); + ~SourceFloat() override = default; + + int32_t onProcess(int32_t numFrames) override; + + const char *getName() override { + return "SourceFloat"; + } +}; + +} /* namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph */ + +#endif //FLOWGRAPH_SOURCE_FLOAT_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SourceI16_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SourceI16_android.cpp new file mode 100644 index 0000000..1c50073 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SourceI16_android.cpp @@ -0,0 +1,54 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include "oboe_flowgraph_FlowGraphNode_android.h" +#include "oboe_flowgraph_SourceI16_android.h" + +#if FLOWGRAPH_ANDROID_INTERNAL +#include +#endif + +using namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph; + +SourceI16::SourceI16(int32_t channelCount) + : FlowGraphSourceBuffered(channelCount) { +} + +int32_t SourceI16::onProcess(int32_t numFrames) { + float *floatData = output.getBuffer(); + int32_t channelCount = output.getSamplesPerFrame(); + + int32_t framesLeft = mSizeInFrames - mFrameIndex; + int32_t framesToProcess = std::min(numFrames, framesLeft); + int32_t numSamples = framesToProcess * channelCount; + + const int16_t *shortBase = static_cast(mData); + const int16_t *shortData = &shortBase[mFrameIndex * channelCount]; + +#if FLOWGRAPH_ANDROID_INTERNAL + memcpy_to_float_from_i16(floatData, shortData, numSamples); +#else + for (int i = 0; i < numSamples; i++) { + *floatData++ = *shortData++ * (1.0f / 32768); + } +#endif + + mFrameIndex += framesToProcess; + return framesToProcess; +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SourceI16_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SourceI16_android.h new file mode 100644 index 0000000..edcf2c5 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SourceI16_android.h @@ -0,0 +1,42 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FLOWGRAPH_SOURCE_I16_H +#define FLOWGRAPH_SOURCE_I16_H + +#include +#include + +#include "oboe_flowgraph_FlowGraphNode_android.h" + +namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph { +/** + * AudioSource that reads a block of pre-defined 16-bit integer data. + */ +class SourceI16 : public FlowGraphSourceBuffered { +public: + explicit SourceI16(int32_t channelCount); + + int32_t onProcess(int32_t numFrames) override; + + const char *getName() override { + return "SourceI16"; + } +}; + +} /* namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph */ + +#endif //FLOWGRAPH_SOURCE_I16_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SourceI24_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SourceI24_android.cpp new file mode 100644 index 0000000..b3a654b --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SourceI24_android.cpp @@ -0,0 +1,65 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include "oboe_flowgraph_FlowGraphNode_android.h" +#include "oboe_flowgraph_SourceI24_android.h" + +#if FLOWGRAPH_ANDROID_INTERNAL +#include +#endif + +using namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph; + +constexpr int kBytesPerI24Packed = 3; + +SourceI24::SourceI24(int32_t channelCount) + : FlowGraphSourceBuffered(channelCount) { +} + +int32_t SourceI24::onProcess(int32_t numFrames) { + float *floatData = output.getBuffer(); + int32_t channelCount = output.getSamplesPerFrame(); + + int32_t framesLeft = mSizeInFrames - mFrameIndex; + int32_t framesToProcess = std::min(numFrames, framesLeft); + int32_t numSamples = framesToProcess * channelCount; + + const uint8_t *byteBase = (uint8_t *) mData; + const uint8_t *byteData = &byteBase[mFrameIndex * channelCount * kBytesPerI24Packed]; + +#if FLOWGRAPH_ANDROID_INTERNAL + memcpy_to_float_from_p24(floatData, byteData, numSamples); +#else + static const float scale = 1. / (float)(1UL << 31); + for (int i = 0; i < numSamples; i++) { + // Assemble the data assuming Little Endian format. + int32_t pad = byteData[2]; + pad <<= 8; + pad |= byteData[1]; + pad <<= 8; + pad |= byteData[0]; + pad <<= 8; // Shift to 32 bit data so the sign is correct. + byteData += kBytesPerI24Packed; + *floatData++ = pad * scale; // scale to range -1.0 to 1.0 + } +#endif + + mFrameIndex += framesToProcess; + return framesToProcess; +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SourceI24_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SourceI24_android.h new file mode 100644 index 0000000..9d98296 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SourceI24_android.h @@ -0,0 +1,43 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FLOWGRAPH_SOURCE_I24_H +#define FLOWGRAPH_SOURCE_I24_H + +#include +#include + +#include "oboe_flowgraph_FlowGraphNode_android.h" + +namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph { + +/** + * AudioSource that reads a block of pre-defined 24-bit packed integer data. + */ +class SourceI24 : public FlowGraphSourceBuffered { +public: + explicit SourceI24(int32_t channelCount); + + int32_t onProcess(int32_t numFrames) override; + + const char *getName() override { + return "SourceI24"; + } +}; + +} /* namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph */ + +#endif //FLOWGRAPH_SOURCE_I24_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SourceI32_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SourceI32_android.cpp new file mode 100644 index 0000000..26f0bb3 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SourceI32_android.cpp @@ -0,0 +1,54 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include "oboe_flowgraph_FlowGraphNode_android.h" +#include "oboe_flowgraph_SourceI32_android.h" + +#if FLOWGRAPH_ANDROID_INTERNAL +#include +#endif + +using namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph; + +SourceI32::SourceI32(int32_t channelCount) + : FlowGraphSourceBuffered(channelCount) { +} + +int32_t SourceI32::onProcess(int32_t numFrames) { + float *floatData = output.getBuffer(); + const int32_t channelCount = output.getSamplesPerFrame(); + + const int32_t framesLeft = mSizeInFrames - mFrameIndex; + const int32_t framesToProcess = std::min(numFrames, framesLeft); + const int32_t numSamples = framesToProcess * channelCount; + + const int32_t *intBase = static_cast(mData); + const int32_t *intData = &intBase[mFrameIndex * channelCount]; + +#if FLOWGRAPH_ANDROID_INTERNAL + memcpy_to_float_from_i32(floatData, intData, numSamples); +#else + for (int i = 0; i < numSamples; i++) { + *floatData++ = *intData++ * kScale; + } +#endif + + mFrameIndex += framesToProcess; + return framesToProcess; +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SourceI32_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SourceI32_android.h new file mode 100644 index 0000000..ed82a08 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SourceI32_android.h @@ -0,0 +1,42 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FLOWGRAPH_SOURCE_I32_H +#define FLOWGRAPH_SOURCE_I32_H + +#include + +#include "oboe_flowgraph_FlowGraphNode_android.h" + +namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph { + +class SourceI32 : public FlowGraphSourceBuffered { +public: + explicit SourceI32(int32_t channelCount); + ~SourceI32() override = default; + + int32_t onProcess(int32_t numFrames) override; + + const char *getName() override { + return "SourceI32"; + } +private: + static constexpr float kScale = 1.0 / (1UL << 31); +}; + +} /* namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph */ + +#endif //FLOWGRAPH_SOURCE_I32_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SourceI8_24_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SourceI8_24_android.cpp new file mode 100644 index 0000000..5a69c55 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SourceI8_24_android.cpp @@ -0,0 +1,54 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include "oboe_flowgraph_FlowGraphNode_android.h" +#include "oboe_flowgraph_SourceI8_24_android.h" + +#if FLOWGRAPH_ANDROID_INTERNAL +#include +#endif + +using namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph; + +SourceI8_24::SourceI8_24(int32_t channelCount) + : FlowGraphSourceBuffered(channelCount) { +} + +int32_t SourceI8_24::onProcess(int32_t numFrames) { + float *floatData = output.getBuffer(); + const int32_t channelCount = output.getSamplesPerFrame(); + + const int32_t framesLeft = mSizeInFrames - mFrameIndex; + const int32_t framesToProcess = std::min(numFrames, framesLeft); + const int32_t numSamples = framesToProcess * channelCount; + + const int32_t *intBase = static_cast(mData); + const int32_t *intData = &intBase[mFrameIndex * channelCount]; + +#if FLOWGRAPH_ANDROID_INTERNAL + memcpy_to_float_from_q8_23(floatData, intData, numSamples); +#else + for (int i = 0; i < numSamples; i++) { + *floatData++ = *intData++ * kScale; + } +#endif + + mFrameIndex += framesToProcess; + return framesToProcess; +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SourceI8_24_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SourceI8_24_android.h new file mode 100644 index 0000000..7cab06f --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_SourceI8_24_android.h @@ -0,0 +1,42 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FLOWGRAPH_SOURCE_I8_24_H +#define FLOWGRAPH_SOURCE_I8_24_H + +#include + +#include "oboe_flowgraph_FlowGraphNode_android.h" + +namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph { + +class SourceI8_24 : public FlowGraphSourceBuffered { +public: + explicit SourceI8_24(int32_t channelCount); + ~SourceI8_24() override = default; + + int32_t onProcess(int32_t numFrames) override; + + const char *getName() override { + return "SourceI8_24"; + } +private: + static constexpr float kScale = 1.0 / (1UL << 23); +}; + +} /* namespace FLOWGRAPH_OUTER_NAMESPACE::flowgraph */ + +#endif //FLOWGRAPH_SOURCE_I8_24_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_HyperbolicCosineWindow_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_HyperbolicCosineWindow_android.h new file mode 100644 index 0000000..f2f94c3 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_HyperbolicCosineWindow_android.h @@ -0,0 +1,71 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef RESAMPLER_HYPERBOLIC_COSINE_WINDOW_H +#define RESAMPLER_HYPERBOLIC_COSINE_WINDOW_H + +#include + +#include "oboe_flowgraph_resampler_ResamplerDefinitions_android.h" + +namespace RESAMPLER_OUTER_NAMESPACE::resampler { + +/** + * Calculate a HyperbolicCosineWindow window centered at 0. + * This can be used in place of a Kaiser window. + * + * The code is based on an anonymous contribution by "a concerned citizen": + * https://dsp.stackexchange.com/questions/37714/kaiser-window-approximation + */ +class HyperbolicCosineWindow { +public: + HyperbolicCosineWindow() { + setStopBandAttenuation(60); + } + + /** + * @param attenuation typical values range from 30 to 90 dB + * @return beta + */ + double setStopBandAttenuation(double attenuation) { + double alpha = ((-325.1e-6 * attenuation + 0.1677) * attenuation) - 3.149; + setAlpha(alpha); + return alpha; + } + + void setAlpha(double alpha) { + mAlpha = alpha; + mInverseCoshAlpha = 1.0 / cosh(alpha); + } + + /** + * @param x ranges from -1.0 to +1.0 + */ + double operator()(double x) { + double x2 = x * x; + if (x2 >= 1.0) return 0.0; + double w = mAlpha * sqrt(1.0 - x2); + return cosh(w) * mInverseCoshAlpha; + } + +private: + double mAlpha = 0.0; + double mInverseCoshAlpha = 1.0; +}; + +} /* namespace RESAMPLER_OUTER_NAMESPACE::resampler */ + +#endif //RESAMPLER_HYPERBOLIC_COSINE_WINDOW_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_IntegerRatio_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_IntegerRatio_android.cpp new file mode 100644 index 0000000..e16307a --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_IntegerRatio_android.cpp @@ -0,0 +1,50 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "oboe_flowgraph_resampler_IntegerRatio_android.h" + +using namespace RESAMPLER_OUTER_NAMESPACE::resampler; + +// Enough primes to cover the common sample rates. +static const int kPrimes[] = { + 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, + 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, + 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, + 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199}; + +void IntegerRatio::reduce() { + for (int prime : kPrimes) { + if (mNumerator < prime || mDenominator < prime) { + break; + } + + // Find biggest prime factor for numerator. + while (true) { + int top = mNumerator / prime; + int bottom = mDenominator / prime; + if ((top >= 1) + && (bottom >= 1) + && (top * prime == mNumerator) // divided evenly? + && (bottom * prime == mDenominator)) { + mNumerator = top; + mDenominator = bottom; + } else { + break; + } + } + + } +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_IntegerRatio_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_IntegerRatio_android.h new file mode 100644 index 0000000..ca10d24 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_IntegerRatio_android.h @@ -0,0 +1,54 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef RESAMPLER_INTEGER_RATIO_H +#define RESAMPLER_INTEGER_RATIO_H + +#include + +#include "oboe_flowgraph_resampler_ResamplerDefinitions_android.h" + +namespace RESAMPLER_OUTER_NAMESPACE::resampler { + +/** + * Represent the ratio of two integers. + */ +class IntegerRatio { +public: + IntegerRatio(int32_t numerator, int32_t denominator) + : mNumerator(numerator), mDenominator(denominator) {} + + /** + * Reduce by removing common prime factors. + */ + void reduce(); + + int32_t getNumerator() { + return mNumerator; + } + + int32_t getDenominator() { + return mDenominator; + } + +private: + int32_t mNumerator; + int32_t mDenominator; +}; + +} /* namespace RESAMPLER_OUTER_NAMESPACE::resampler */ + +#endif //RESAMPLER_INTEGER_RATIO_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_KaiserWindow_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_KaiserWindow_android.h new file mode 100644 index 0000000..059c8fc --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_KaiserWindow_android.h @@ -0,0 +1,90 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef RESAMPLER_KAISER_WINDOW_H +#define RESAMPLER_KAISER_WINDOW_H + +#include + +#include "oboe_flowgraph_resampler_ResamplerDefinitions_android.h" + +namespace RESAMPLER_OUTER_NAMESPACE::resampler { + +/** + * Calculate a Kaiser window centered at 0. + */ +class KaiserWindow { +public: + KaiserWindow() { + setStopBandAttenuation(60); + } + + /** + * @param attenuation typical values range from 30 to 90 dB + * @return beta + */ + double setStopBandAttenuation(double attenuation) { + double beta = 0.0; + if (attenuation > 50) { + beta = 0.1102 * (attenuation - 8.7); + } else if (attenuation >= 21) { + double a21 = attenuation - 21; + beta = 0.5842 * pow(a21, 0.4) + (0.07886 * a21); + } + setBeta(beta); + return beta; + } + + void setBeta(double beta) { + mBeta = beta; + mInverseBesselBeta = 1.0 / bessel(beta); + } + + /** + * @param x ranges from -1.0 to +1.0 + */ + double operator()(double x) { + double x2 = x * x; + if (x2 >= 1.0) return 0.0; + double w = mBeta * sqrt(1.0 - x2); + return bessel(w) * mInverseBesselBeta; + } + + // Approximation of a + // modified zero order Bessel function of the first kind. + // Based on a discussion at: + // https://dsp.stackexchange.com/questions/37714/kaiser-window-approximation + static double bessel(double x) { + double y = cosh(0.970941817426052 * x); + y += cosh(0.8854560256532099 * x); + y += cosh(0.7485107481711011 * x); + y += cosh(0.5680647467311558 * x); + y += cosh(0.3546048870425356 * x); + y += cosh(0.120536680255323 * x); + y *= 2; + y += cosh(x); + y /= 13; + return y; + } + +private: + double mBeta = 0.0; + double mInverseBesselBeta = 1.0; +}; + +} /* namespace RESAMPLER_OUTER_NAMESPACE::resampler */ + +#endif //RESAMPLER_KAISER_WINDOW_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_LinearResampler_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_LinearResampler_android.cpp new file mode 100644 index 0000000..616a762 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_LinearResampler_android.cpp @@ -0,0 +1,42 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "oboe_flowgraph_resampler_LinearResampler_android.h" + +using namespace RESAMPLER_OUTER_NAMESPACE::resampler; + +LinearResampler::LinearResampler(const MultiChannelResampler::Builder &builder) + : MultiChannelResampler(builder) { + mPreviousFrame = std::make_unique(getChannelCount()); + mCurrentFrame = std::make_unique(getChannelCount()); +} + +void LinearResampler::writeFrame(const float *frame) { + memcpy(mPreviousFrame.get(), mCurrentFrame.get(), sizeof(float) * getChannelCount()); + memcpy(mCurrentFrame.get(), frame, sizeof(float) * getChannelCount()); +} + +void LinearResampler::readFrame(float *frame) { + float *previous = mPreviousFrame.get(); + float *current = mCurrentFrame.get(); + float phase = (float) getIntegerPhase() / mDenominator; + // iterate across samples in the frame + for (int channel = 0; channel < getChannelCount(); channel++) { + float f0 = *previous++; + float f1 = *current++; + *frame++ = f0 + (phase * (f1 - f0)); + } +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_LinearResampler_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_LinearResampler_android.h new file mode 100644 index 0000000..51a7cce --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_LinearResampler_android.h @@ -0,0 +1,47 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef RESAMPLER_LINEAR_RESAMPLER_H +#define RESAMPLER_LINEAR_RESAMPLER_H + +#include +#include +#include + +#include "oboe_flowgraph_resampler_MultiChannelResampler_android.h" +#include "oboe_flowgraph_resampler_ResamplerDefinitions_android.h" + +namespace RESAMPLER_OUTER_NAMESPACE::resampler { + +/** + * Simple resampler that uses bi-linear interpolation. + */ +class LinearResampler : public MultiChannelResampler { +public: + explicit LinearResampler(const MultiChannelResampler::Builder &builder); + + void writeFrame(const float *frame) override; + + void readFrame(float *frame) override; + +private: + std::unique_ptr mPreviousFrame; + std::unique_ptr mCurrentFrame; +}; + +} /* namespace RESAMPLER_OUTER_NAMESPACE::resampler */ + +#endif //RESAMPLER_LINEAR_RESAMPLER_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_MultiChannelResampler_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_MultiChannelResampler_android.cpp new file mode 100644 index 0000000..89b947a --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_MultiChannelResampler_android.cpp @@ -0,0 +1,171 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "oboe_flowgraph_resampler_IntegerRatio_android.h" +#include "oboe_flowgraph_resampler_LinearResampler_android.h" +#include "oboe_flowgraph_resampler_MultiChannelResampler_android.h" +#include "oboe_flowgraph_resampler_PolyphaseResampler_android.h" +#include "oboe_flowgraph_resampler_PolyphaseResamplerMono_android.h" +#include "oboe_flowgraph_resampler_PolyphaseResamplerStereo_android.h" +#include "oboe_flowgraph_resampler_SincResampler_android.h" +#include "oboe_flowgraph_resampler_SincResamplerStereo_android.h" + +using namespace RESAMPLER_OUTER_NAMESPACE::resampler; + +MultiChannelResampler::MultiChannelResampler(const MultiChannelResampler::Builder &builder) + : mNumTaps(builder.getNumTaps()) + , mX(static_cast(builder.getChannelCount()) + * static_cast(builder.getNumTaps()) * 2) + , mSingleFrame(builder.getChannelCount()) + , mChannelCount(builder.getChannelCount()) + { + // Reduce sample rates to the smallest ratio. + // For example 44100/48000 would become 147/160. + IntegerRatio ratio(builder.getInputRate(), builder.getOutputRate()); + ratio.reduce(); + mNumerator = ratio.getNumerator(); + mDenominator = ratio.getDenominator(); + mIntegerPhase = mDenominator; // so we start with a write needed +} + +// static factory method +MultiChannelResampler *MultiChannelResampler::make(int32_t channelCount, + int32_t inputRate, + int32_t outputRate, + Quality quality) { + Builder builder; + builder.setInputRate(inputRate); + builder.setOutputRate(outputRate); + builder.setChannelCount(channelCount); + + switch (quality) { + case Quality::Fastest: + builder.setNumTaps(2); + break; + case Quality::Low: + builder.setNumTaps(4); + break; + case Quality::Medium: + default: + builder.setNumTaps(8); + break; + case Quality::High: + builder.setNumTaps(16); + break; + case Quality::Best: + builder.setNumTaps(32); + break; + } + + // Set the cutoff frequency so that we do not get aliasing when down-sampling. + if (inputRate > outputRate) { + builder.setNormalizedCutoff(kDefaultNormalizedCutoff); + } + return builder.build(); +} + +MultiChannelResampler *MultiChannelResampler::Builder::build() { + if (getNumTaps() == 2) { + // Note that this does not do low pass filteringh. + return new LinearResampler(*this); + } + IntegerRatio ratio(getInputRate(), getOutputRate()); + ratio.reduce(); + bool usePolyphase = (getNumTaps() * ratio.getDenominator()) <= kMaxCoefficients; + if (usePolyphase) { + if (getChannelCount() == 1) { + return new PolyphaseResamplerMono(*this); + } else if (getChannelCount() == 2) { + return new PolyphaseResamplerStereo(*this); + } else { + return new PolyphaseResampler(*this); + } + } else { + // Use less optimized resampler that uses a float phaseIncrement. + // TODO mono resampler + if (getChannelCount() == 2) { + return new SincResamplerStereo(*this); + } else { + return new SincResampler(*this); + } + } +} + +void MultiChannelResampler::writeFrame(const float *frame) { + // Move cursor before write so that cursor points to last written frame in read. + if (--mCursor < 0) { + mCursor = getNumTaps() - 1; + } + float *dest = &mX[static_cast(mCursor) * static_cast(getChannelCount())]; + int offset = getNumTaps() * getChannelCount(); + for (int channel = 0; channel < getChannelCount(); channel++) { + // Write twice so we avoid having to wrap when reading. + dest[channel] = dest[channel + offset] = frame[channel]; + } +} + +float MultiChannelResampler::sinc(float radians) { + if (fabsf(radians) < 1.0e-9f) return 1.0f; // avoid divide by zero + return sinf(radians) / radians; // Sinc function +} + +// Generate coefficients in the order they will be used by readFrame(). +// This is more complicated but readFrame() is called repeatedly and should be optimized. +void MultiChannelResampler::generateCoefficients(int32_t inputRate, + int32_t outputRate, + int32_t numRows, + double phaseIncrement, + float normalizedCutoff) { + mCoefficients.resize(static_cast(getNumTaps()) * static_cast(numRows)); + int coefficientIndex = 0; + double phase = 0.0; // ranges from 0.0 to 1.0, fraction between samples + // Stretch the sinc function for low pass filtering. + const float cutoffScaler = (outputRate < inputRate) + ? (normalizedCutoff * (float)outputRate / inputRate) + : 1.0f; // Do not filter when upsampling. + const int numTapsHalf = getNumTaps() / 2; // numTaps must be even. + const float numTapsHalfInverse = 1.0f / numTapsHalf; + for (int i = 0; i < numRows; i++) { + float tapPhase = phase - numTapsHalf; + float gain = 0.0; // sum of raw coefficients + int gainCursor = coefficientIndex; + for (int tap = 0; tap < getNumTaps(); tap++) { + float radians = tapPhase * M_PI; + +#if MCR_USE_KAISER + float window = mKaiserWindow(tapPhase * numTapsHalfInverse); +#else + float window = mCoshWindow(static_cast(tapPhase) * numTapsHalfInverse); +#endif + float coefficient = sinc(radians * cutoffScaler) * window; + mCoefficients.at(coefficientIndex++) = coefficient; + gain += coefficient; + tapPhase += 1.0; + } + phase += phaseIncrement; + while (phase >= 1.0) { + phase -= 1.0; + } + + // Correct for gain variations. + float gainCorrection = 1.0 / gain; // normalize the gain + for (int tap = 0; tap < getNumTaps(); tap++) { + mCoefficients.at(gainCursor + tap) *= gainCorrection; + } + } +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_MultiChannelResampler_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_MultiChannelResampler_android.h new file mode 100644 index 0000000..332c589 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_MultiChannelResampler_android.h @@ -0,0 +1,281 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef RESAMPLER_MULTICHANNEL_RESAMPLER_H +#define RESAMPLER_MULTICHANNEL_RESAMPLER_H + +#include +#include +#include +#include + +#ifndef MCR_USE_KAISER +// It appears from the spectrogram that the HyperbolicCosine window leads to fewer artifacts. +// And it is faster to calculate. +#define MCR_USE_KAISER 0 +#endif + +#if MCR_USE_KAISER +#include "oboe_flowgraph_resampler_KaiserWindow_android.h" +#else +#include "oboe_flowgraph_resampler_HyperbolicCosineWindow_android.h" +#endif + +#include "oboe_flowgraph_resampler_ResamplerDefinitions_android.h" + +namespace RESAMPLER_OUTER_NAMESPACE::resampler { + +class MultiChannelResampler { + +public: + + enum class Quality : int32_t { + Fastest, + Low, + Medium, + High, + Best, + }; + + class Builder { + public: + /** + * Construct an optimal resampler based on the specified parameters. + * @return address of a resampler + */ + MultiChannelResampler *build(); + + /** + * The number of taps in the resampling filter. + * More taps gives better quality but uses more CPU time. + * This typically ranges from 4 to 64. Default is 16. + * + * For polyphase filters, numTaps must be a multiple of four for loop unrolling. + * @param numTaps number of taps for the filter + * @return address of this builder for chaining calls + */ + Builder *setNumTaps(int32_t numTaps) { + mNumTaps = numTaps; + return this; + } + + /** + * Use 1 for mono, 2 for stereo, etc. Default is 1. + * + * @param channelCount number of channels + * @return address of this builder for chaining calls + */ + Builder *setChannelCount(int32_t channelCount) { + mChannelCount = channelCount; + return this; + } + + /** + * Default is 48000. + * + * @param inputRate sample rate of the input stream + * @return address of this builder for chaining calls + */ + Builder *setInputRate(int32_t inputRate) { + mInputRate = inputRate; + return this; + } + + /** + * Default is 48000. + * + * @param outputRate sample rate of the output stream + * @return address of this builder for chaining calls + */ + Builder *setOutputRate(int32_t outputRate) { + mOutputRate = outputRate; + return this; + } + + /** + * Set cutoff frequency relative to the Nyquist rate of the output sample rate. + * Set to 1.0 to match the Nyquist frequency. + * Set lower to reduce aliasing. + * Default is 0.70. + * + * Note that this value is ignored when upsampling, which is when + * the outputRate is higher than the inputRate. + * + * @param normalizedCutoff anti-aliasing filter cutoff + * @return address of this builder for chaining calls + */ + Builder *setNormalizedCutoff(float normalizedCutoff) { + mNormalizedCutoff = normalizedCutoff; + return this; + } + + int32_t getNumTaps() const { + return mNumTaps; + } + + int32_t getChannelCount() const { + return mChannelCount; + } + + int32_t getInputRate() const { + return mInputRate; + } + + int32_t getOutputRate() const { + return mOutputRate; + } + + float getNormalizedCutoff() const { + return mNormalizedCutoff; + } + + protected: + int32_t mChannelCount = 1; + int32_t mNumTaps = 16; + int32_t mInputRate = 48000; + int32_t mOutputRate = 48000; + float mNormalizedCutoff = kDefaultNormalizedCutoff; + }; + + virtual ~MultiChannelResampler() = default; + + /** + * Factory method for making a resampler that is optimal for the given inputs. + * + * @param channelCount number of channels, 2 for stereo + * @param inputRate sample rate of the input stream + * @param outputRate sample rate of the output stream + * @param quality higher quality sounds better but uses more CPU + * @return an optimal resampler + */ + static MultiChannelResampler *make(int32_t channelCount, + int32_t inputRate, + int32_t outputRate, + Quality quality); + + bool isWriteNeeded() const { + return mIntegerPhase >= mDenominator; + } + + /** + * Write a frame containing N samples. + * + * @param frame pointer to the first sample in a frame + */ + void writeNextFrame(const float *frame) { + writeFrame(frame); + advanceWrite(); + } + + /** + * Read a frame containing N samples. + * + * @param frame pointer to the first sample in a frame + */ + void readNextFrame(float *frame) { + readFrame(frame); + advanceRead(); + } + + int getNumTaps() const { + return mNumTaps; + } + + int getChannelCount() const { + return mChannelCount; + } + + static float hammingWindow(float radians, float spread); + + static float sinc(float radians); + +protected: + + explicit MultiChannelResampler(const MultiChannelResampler::Builder &builder); + + /** + * Write a frame containing N samples. + * Call advanceWrite() after calling this. + * @param frame pointer to the first sample in a frame + */ + virtual void writeFrame(const float *frame); + + /** + * Read a frame containing N samples using interpolation. + * Call advanceRead() after calling this. + * @param frame pointer to the first sample in a frame + */ + virtual void readFrame(float *frame) = 0; + + void advanceWrite() { + mIntegerPhase -= mDenominator; + } + + void advanceRead() { + mIntegerPhase += mNumerator; + } + + /** + * Generate the filter coefficients in optimal order. + * + * Note that normalizedCutoff is ignored when upsampling, which is when + * the outputRate is higher than the inputRate. + * + * @param inputRate sample rate of the input stream + * @param outputRate sample rate of the output stream + * @param numRows number of rows in the array that contain a set of tap coefficients + * @param phaseIncrement how much to increment the phase between rows + * @param normalizedCutoff filter cutoff frequency normalized to Nyquist rate of output + */ + void generateCoefficients(int32_t inputRate, + int32_t outputRate, + int32_t numRows, + double phaseIncrement, + float normalizedCutoff); + + + int32_t getIntegerPhase() { + return mIntegerPhase; + } + + static constexpr int kMaxCoefficients = 8 * 1024; + std::vector mCoefficients; + + const int mNumTaps; + int mCursor = 0; + std::vector mX; // delayed input values for the FIR + std::vector mSingleFrame; // one frame for temporary use + int32_t mIntegerPhase = 0; + int32_t mNumerator = 0; + int32_t mDenominator = 0; + + +private: + +#if MCR_USE_KAISER + KaiserWindow mKaiserWindow; +#else + HyperbolicCosineWindow mCoshWindow; +#endif + + static constexpr float kDefaultNormalizedCutoff = 0.70f; + + const int mChannelCount; +}; + +} /* namespace RESAMPLER_OUTER_NAMESPACE::resampler */ + +#endif //RESAMPLER_MULTICHANNEL_RESAMPLER_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_PolyphaseResamplerMono_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_PolyphaseResamplerMono_android.cpp new file mode 100644 index 0000000..6e149ec --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_PolyphaseResamplerMono_android.cpp @@ -0,0 +1,63 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "oboe_flowgraph_resampler_PolyphaseResamplerMono_android.h" + +using namespace RESAMPLER_OUTER_NAMESPACE::resampler; + +#define MONO 1 + +PolyphaseResamplerMono::PolyphaseResamplerMono(const MultiChannelResampler::Builder &builder) + : PolyphaseResampler(builder) { + assert(builder.getChannelCount() == MONO); +} + +void PolyphaseResamplerMono::writeFrame(const float *frame) { + // Move cursor before write so that cursor points to last written frame in read. + if (--mCursor < 0) { + mCursor = getNumTaps() - 1; + } + float *dest = &mX[mCursor * MONO]; + const int offset = mNumTaps * MONO; + // Write each channel twice so we avoid having to wrap when running the FIR. + const float sample = frame[0]; + // Put ordered writes together. + dest[0] = sample; + dest[offset] = sample; +} + +void PolyphaseResamplerMono::readFrame(float *frame) { + // Clear accumulator. + float sum = 0.0; + + // Multiply input times precomputed windowed sinc function. + const float *coefficients = &mCoefficients[mCoefficientCursor]; + float *xFrame = &mX[mCursor * MONO]; + const int numLoops = mNumTaps >> 2; // n/4 + for (int i = 0; i < numLoops; i++) { + // Manual loop unrolling, might get converted to SIMD. + sum += *xFrame++ * *coefficients++; + sum += *xFrame++ * *coefficients++; + sum += *xFrame++ * *coefficients++; + sum += *xFrame++ * *coefficients++; + } + + mCoefficientCursor = (mCoefficientCursor + mNumTaps) % mCoefficients.size(); + + // Copy accumulator to output. + frame[0] = sum; +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_PolyphaseResamplerMono_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_PolyphaseResamplerMono_android.h new file mode 100644 index 0000000..d3195bd --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_PolyphaseResamplerMono_android.h @@ -0,0 +1,41 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef RESAMPLER_POLYPHASE_RESAMPLER_MONO_H +#define RESAMPLER_POLYPHASE_RESAMPLER_MONO_H + +#include +#include + +#include "oboe_flowgraph_resampler_PolyphaseResampler_android.h" +#include "oboe_flowgraph_resampler_ResamplerDefinitions_android.h" + +namespace RESAMPLER_OUTER_NAMESPACE::resampler { + +class PolyphaseResamplerMono : public PolyphaseResampler { +public: + explicit PolyphaseResamplerMono(const MultiChannelResampler::Builder &builder); + + virtual ~PolyphaseResamplerMono() = default; + + void writeFrame(const float *frame) override; + + void readFrame(float *frame) override; +}; + +} /* namespace RESAMPLER_OUTER_NAMESPACE::resampler */ + +#endif //RESAMPLER_POLYPHASE_RESAMPLER_MONO_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_PolyphaseResamplerStereo_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_PolyphaseResamplerStereo_android.cpp new file mode 100644 index 0000000..e02aa66 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_PolyphaseResamplerStereo_android.cpp @@ -0,0 +1,79 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "oboe_flowgraph_resampler_PolyphaseResamplerStereo_android.h" + +using namespace RESAMPLER_OUTER_NAMESPACE::resampler; + +#define STEREO 2 + +PolyphaseResamplerStereo::PolyphaseResamplerStereo(const MultiChannelResampler::Builder &builder) + : PolyphaseResampler(builder) { + assert(builder.getChannelCount() == STEREO); +} + +void PolyphaseResamplerStereo::writeFrame(const float *frame) { + // Move cursor before write so that cursor points to last written frame in read. + if (--mCursor < 0) { + mCursor = getNumTaps() - 1; + } + float *dest = &mX[mCursor * STEREO]; + const int offset = mNumTaps * STEREO; + // Write each channel twice so we avoid having to wrap when running the FIR. + const float left = frame[0]; + const float right = frame[1]; + // Put ordered writes together. + dest[0] = left; + dest[1] = right; + dest[offset] = left; + dest[1 + offset] = right; +} + +void PolyphaseResamplerStereo::readFrame(float *frame) { + // Clear accumulators. + float left = 0.0; + float right = 0.0; + + // Multiply input times precomputed windowed sinc function. + const float *coefficients = &mCoefficients[mCoefficientCursor]; + float *xFrame = &mX[mCursor * STEREO]; + const int numLoops = mNumTaps >> 2; // n/4 + for (int i = 0; i < numLoops; i++) { + // Manual loop unrolling, might get converted to SIMD. + float coefficient = *coefficients++; + left += *xFrame++ * coefficient; + right += *xFrame++ * coefficient; + + coefficient = *coefficients++; // next tap + left += *xFrame++ * coefficient; + right += *xFrame++ * coefficient; + + coefficient = *coefficients++; // next tap + left += *xFrame++ * coefficient; + right += *xFrame++ * coefficient; + + coefficient = *coefficients++; // next tap + left += *xFrame++ * coefficient; + right += *xFrame++ * coefficient; + } + + mCoefficientCursor = (mCoefficientCursor + mNumTaps) % mCoefficients.size(); + + // Copy accumulators to output. + frame[0] = left; + frame[1] = right; +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_PolyphaseResamplerStereo_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_PolyphaseResamplerStereo_android.h new file mode 100644 index 0000000..a8d0101 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_PolyphaseResamplerStereo_android.h @@ -0,0 +1,41 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef RESAMPLER_POLYPHASE_RESAMPLER_STEREO_H +#define RESAMPLER_POLYPHASE_RESAMPLER_STEREO_H + +#include +#include + +#include "oboe_flowgraph_resampler_PolyphaseResampler_android.h" +#include "oboe_flowgraph_resampler_ResamplerDefinitions_android.h" + +namespace RESAMPLER_OUTER_NAMESPACE::resampler { + +class PolyphaseResamplerStereo : public PolyphaseResampler { +public: + explicit PolyphaseResamplerStereo(const MultiChannelResampler::Builder &builder); + + virtual ~PolyphaseResamplerStereo() = default; + + void writeFrame(const float *frame) override; + + void readFrame(float *frame) override; +}; + +} /* namespace RESAMPLER_OUTER_NAMESPACE::resampler */ + +#endif //RESAMPLER_POLYPHASE_RESAMPLER_STEREO_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_PolyphaseResampler_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_PolyphaseResampler_android.cpp new file mode 100644 index 0000000..c96d88f --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_PolyphaseResampler_android.cpp @@ -0,0 +1,61 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include // Do NOT delete. Needed for LLVM. See #1746 +#include +#include +#include "oboe_flowgraph_resampler_IntegerRatio_android.h" +#include "oboe_flowgraph_resampler_PolyphaseResampler_android.h" + +using namespace RESAMPLER_OUTER_NAMESPACE::resampler; + +PolyphaseResampler::PolyphaseResampler(const MultiChannelResampler::Builder &builder) + : MultiChannelResampler(builder) + { + assert((getNumTaps() % 4) == 0); // Required for loop unrolling. + + int32_t inputRate = builder.getInputRate(); + int32_t outputRate = builder.getOutputRate(); + + int32_t numRows = mDenominator; + double phaseIncrement = (double) inputRate / (double) outputRate; + generateCoefficients(inputRate, outputRate, + numRows, phaseIncrement, + builder.getNormalizedCutoff()); +} + +void PolyphaseResampler::readFrame(float *frame) { + // Clear accumulator for mixing. + std::fill(mSingleFrame.begin(), mSingleFrame.end(), 0.0); + + // Multiply input times windowed sinc function. + float *coefficients = &mCoefficients[mCoefficientCursor]; + float *xFrame = &mX[static_cast(mCursor) * static_cast(getChannelCount())]; + for (int i = 0; i < mNumTaps; i++) { + float coefficient = *coefficients++; + for (int channel = 0; channel < getChannelCount(); channel++) { + mSingleFrame[channel] += *xFrame++ * coefficient; + } + } + + // Advance and wrap through coefficients. + mCoefficientCursor = (mCoefficientCursor + mNumTaps) % mCoefficients.size(); + + // Copy accumulator to output. + for (int channel = 0; channel < getChannelCount(); channel++) { + frame[channel] = mSingleFrame[channel]; + } +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_PolyphaseResampler_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_PolyphaseResampler_android.h new file mode 100644 index 0000000..74cdd7d --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_PolyphaseResampler_android.h @@ -0,0 +1,53 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef RESAMPLER_POLYPHASE_RESAMPLER_H +#define RESAMPLER_POLYPHASE_RESAMPLER_H + +#include +#include +#include +#include + +#include "oboe_flowgraph_resampler_MultiChannelResampler_android.h" +#include "oboe_flowgraph_resampler_ResamplerDefinitions_android.h" + +namespace RESAMPLER_OUTER_NAMESPACE::resampler { +/** + * Resampler that is optimized for a reduced ratio of sample rates. + * All of the coefficients for each possible phase value are pre-calculated. + */ +class PolyphaseResampler : public MultiChannelResampler { +public: + /** + * + * @param builder containing lots of parameters + */ + explicit PolyphaseResampler(const MultiChannelResampler::Builder &builder); + + virtual ~PolyphaseResampler() = default; + + void readFrame(float *frame) override; + +protected: + + int32_t mCoefficientCursor = 0; + +}; + +} /* namespace RESAMPLER_OUTER_NAMESPACE::resampler */ + +#endif //RESAMPLER_POLYPHASE_RESAMPLER_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_ResamplerDefinitions_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_ResamplerDefinitions_android.h new file mode 100644 index 0000000..c6791ec --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_ResamplerDefinitions_android.h @@ -0,0 +1,27 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Set flag RESAMPLER_OUTER_NAMESPACE based on whether compiler flag +// __ANDROID_NDK__ is defined. __ANDROID_NDK__ should be defined in oboe +// but not in android. + +#ifndef RESAMPLER_OUTER_NAMESPACE +#ifdef __ANDROID_NDK__ +#define RESAMPLER_OUTER_NAMESPACE oboe +#else +#define RESAMPLER_OUTER_NAMESPACE aaudio +#endif // __ANDROID_NDK__ +#endif // RESAMPLER_OUTER_NAMESPACE diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_SincResamplerStereo_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_SincResamplerStereo_android.cpp new file mode 100644 index 0000000..925946b --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_SincResamplerStereo_android.cpp @@ -0,0 +1,81 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include // Do NOT delete. Needed for LLVM. See #1746 +#include +#include + +#include "oboe_flowgraph_resampler_SincResamplerStereo_android.h" + +using namespace RESAMPLER_OUTER_NAMESPACE::resampler; + +#define STEREO 2 + +SincResamplerStereo::SincResamplerStereo(const MultiChannelResampler::Builder &builder) + : SincResampler(builder) { + assert(builder.getChannelCount() == STEREO); +} + +void SincResamplerStereo::writeFrame(const float *frame) { + // Move cursor before write so that cursor points to last written frame in read. + if (--mCursor < 0) { + mCursor = getNumTaps() - 1; + } + float *dest = &mX[mCursor * STEREO]; + const int offset = mNumTaps * STEREO; + // Write each channel twice so we avoid having to wrap when running the FIR. + const float left = frame[0]; + const float right = frame[1]; + // Put ordered writes together. + dest[0] = left; + dest[1] = right; + dest[offset] = left; + dest[1 + offset] = right; +} + +// Multiply input times windowed sinc function. +void SincResamplerStereo::readFrame(float *frame) { + // Clear accumulator for mixing. + std::fill(mSingleFrame.begin(), mSingleFrame.end(), 0.0); + std::fill(mSingleFrame2.begin(), mSingleFrame2.end(), 0.0); + + // Determine indices into coefficients table. + double tablePhase = getIntegerPhase() * mPhaseScaler; + int index1 = static_cast(floor(tablePhase)); + float *coefficients1 = &mCoefficients[static_cast(index1) + * static_cast(getNumTaps())]; + int index2 = (index1 + 1); + float *coefficients2 = &mCoefficients[static_cast(index2) + * static_cast(getNumTaps())]; + float *xFrame = &mX[static_cast(mCursor) * static_cast(getChannelCount())]; + for (int i = 0; i < mNumTaps; i++) { + float coefficient1 = *coefficients1++; + float coefficient2 = *coefficients2++; + for (int channel = 0; channel < getChannelCount(); channel++) { + float sample = *xFrame++; + mSingleFrame[channel] += sample * coefficient1; + mSingleFrame2[channel] += sample * coefficient2; + } + } + + // Interpolate and copy to output. + float fraction = tablePhase - index1; + for (int channel = 0; channel < getChannelCount(); channel++) { + float low = mSingleFrame[channel]; + float high = mSingleFrame2[channel]; + frame[channel] = low + (fraction * (high - low)); + } +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_SincResamplerStereo_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_SincResamplerStereo_android.h new file mode 100644 index 0000000..16d3d37 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_SincResamplerStereo_android.h @@ -0,0 +1,42 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef RESAMPLER_SINC_RESAMPLER_STEREO_H +#define RESAMPLER_SINC_RESAMPLER_STEREO_H + +#include +#include + +#include "oboe_flowgraph_resampler_SincResampler_android.h" +#include "oboe_flowgraph_resampler_ResamplerDefinitions_android.h" + +namespace RESAMPLER_OUTER_NAMESPACE::resampler { + +class SincResamplerStereo : public SincResampler { +public: + explicit SincResamplerStereo(const MultiChannelResampler::Builder &builder); + + virtual ~SincResamplerStereo() = default; + + void writeFrame(const float *frame) override; + + void readFrame(float *frame) override; + +}; + +} /* namespace RESAMPLER_OUTER_NAMESPACE::resampler */ + +#endif //RESAMPLER_SINC_RESAMPLER_STEREO_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_SincResampler_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_SincResampler_android.cpp new file mode 100644 index 0000000..ca063bb --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_SincResampler_android.cpp @@ -0,0 +1,72 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include // Do NOT delete. Needed for LLVM. See #1746 +#include +#include +#include "oboe_flowgraph_resampler_SincResampler_android.h" + +using namespace RESAMPLER_OUTER_NAMESPACE::resampler; + +SincResampler::SincResampler(const MultiChannelResampler::Builder &builder) + : MultiChannelResampler(builder) + , mSingleFrame2(builder.getChannelCount()) { + assert((getNumTaps() % 4) == 0); // Required for loop unrolling. + mNumRows = kMaxCoefficients / getNumTaps(); // includes guard row + const int32_t numRowsNoGuard = mNumRows - 1; + mPhaseScaler = (double) numRowsNoGuard / mDenominator; + const double phaseIncrement = 1.0 / numRowsNoGuard; + generateCoefficients(builder.getInputRate(), + builder.getOutputRate(), + mNumRows, + phaseIncrement, + builder.getNormalizedCutoff()); +} + +void SincResampler::readFrame(float *frame) { + // Clear accumulator for mixing. + std::fill(mSingleFrame.begin(), mSingleFrame.end(), 0.0); + std::fill(mSingleFrame2.begin(), mSingleFrame2.end(), 0.0); + + // Determine indices into coefficients table. + const double tablePhase = getIntegerPhase() * mPhaseScaler; + const int indexLow = static_cast(floor(tablePhase)); + const int indexHigh = indexLow + 1; // OK because using a guard row. + assert (indexHigh < mNumRows); + float *coefficientsLow = &mCoefficients[static_cast(indexLow) + * static_cast(getNumTaps())]; + float *coefficientsHigh = &mCoefficients[static_cast(indexHigh) + * static_cast(getNumTaps())]; + + float *xFrame = &mX[static_cast(mCursor) * static_cast(getChannelCount())]; + for (int tap = 0; tap < mNumTaps; tap++) { + const float coefficientLow = *coefficientsLow++; + const float coefficientHigh = *coefficientsHigh++; + for (int channel = 0; channel < getChannelCount(); channel++) { + const float sample = *xFrame++; + mSingleFrame[channel] += sample * coefficientLow; + mSingleFrame2[channel] += sample * coefficientHigh; + } + } + + // Interpolate and copy to output. + const float fraction = tablePhase - indexLow; + for (int channel = 0; channel < getChannelCount(); channel++) { + const float low = mSingleFrame[channel]; + const float high = mSingleFrame2[channel]; + frame[channel] = low + (fraction * (high - low)); + } +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_SincResampler_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_SincResampler_android.h new file mode 100644 index 0000000..c8a9a2c --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_flowgraph_resampler_SincResampler_android.h @@ -0,0 +1,50 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef RESAMPLER_SINC_RESAMPLER_H +#define RESAMPLER_SINC_RESAMPLER_H + +#include +#include +#include + +#include "oboe_flowgraph_resampler_MultiChannelResampler_android.h" +#include "oboe_flowgraph_resampler_ResamplerDefinitions_android.h" + +namespace RESAMPLER_OUTER_NAMESPACE::resampler { + +/** + * Resampler that can interpolate between coefficients. + * This can be used to support arbitrary ratios. + */ +class SincResampler : public MultiChannelResampler { +public: + explicit SincResampler(const MultiChannelResampler::Builder &builder); + + virtual ~SincResampler() = default; + + void readFrame(float *frame) override; + +protected: + + std::vector mSingleFrame2; // for interpolation + int32_t mNumRows = 0; + double mPhaseScaler = 1.0; +}; + +} /* namespace RESAMPLER_OUTER_NAMESPACE::resampler */ + +#endif //RESAMPLER_SINC_RESAMPLER_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_AudioClock_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_AudioClock_android.h new file mode 100644 index 0000000..6db1709 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_AudioClock_android.h @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_AUDIO_CLOCK_H +#define OBOE_AUDIO_CLOCK_H + +#include +#include +#include "oboe_oboe_Definitions_android.h" + +namespace oboe { + +class AudioClock { +public: + static int64_t getNanoseconds(clockid_t clockId = CLOCK_MONOTONIC) { + struct timespec time; + int result = clock_gettime(clockId, &time); + if (result < 0) { + return result; + } + return (time.tv_sec * kNanosPerSecond) + time.tv_nsec; + } + + /** + * Sleep until the specified time. + * + * @param nanoTime time to wake up + * @param clockId CLOCK_MONOTONIC is default + * @return 0 or a negative error, eg. -EINTR + */ + + static int sleepUntilNanoTime(int64_t nanoTime, clockid_t clockId = CLOCK_MONOTONIC) { + struct timespec time; + time.tv_sec = nanoTime / kNanosPerSecond; + time.tv_nsec = nanoTime - (time.tv_sec * kNanosPerSecond); + return 0 - clock_nanosleep(clockId, TIMER_ABSTIME, &time, NULL); + } + + /** + * Sleep for the specified number of nanoseconds in real-time. + * Return immediately with 0 if a negative nanoseconds is specified. + * + * @param nanoseconds time to sleep + * @param clockId CLOCK_REALTIME is default + * @return 0 or a negative error, eg. -EINTR + */ + + static int sleepForNanos(int64_t nanoseconds, clockid_t clockId = CLOCK_REALTIME) { + if (nanoseconds > 0) { + struct timespec time; + time.tv_sec = nanoseconds / kNanosPerSecond; + time.tv_nsec = nanoseconds - (time.tv_sec * kNanosPerSecond); + return 0 - clock_nanosleep(clockId, 0, &time, NULL); + } + return 0; + } +}; + +} // namespace oboe + +#endif //OBOE_AUDIO_CLOCK_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_AudioStreamBase_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_AudioStreamBase_android.h new file mode 100644 index 0000000..4bdb8d5 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_AudioStreamBase_android.h @@ -0,0 +1,422 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_STREAM_BASE_H_ +#define OBOE_STREAM_BASE_H_ + +#include +#include +#include +#include "oboe_oboe_AudioStreamCallback_android.h" +#include "oboe_oboe_Definitions_android.h" + +namespace oboe { + +/** + * Base class containing parameters for audio streams and builders. + **/ +class AudioStreamBase { + +public: + + AudioStreamBase() {} + + virtual ~AudioStreamBase() = default; + + // This class only contains primitives so we can use default constructor and copy methods. + + /** + * Default copy constructor + */ + AudioStreamBase(const AudioStreamBase&) = default; + + /** + * Default assignment operator + */ + AudioStreamBase& operator=(const AudioStreamBase&) = default; + + /** + * @return number of channels, for example 2 for stereo, or kUnspecified + */ + int32_t getChannelCount() const { return mChannelCount; } + + /** + * @return Direction::Input or Direction::Output + */ + Direction getDirection() const { return mDirection; } + + /** + * @return sample rate for the stream or kUnspecified + */ + int32_t getSampleRate() const { return mSampleRate; } + + /** + * @deprecated use `getFramesPerDataCallback` instead. + */ + int32_t getFramesPerCallback() const { return getFramesPerDataCallback(); } + + /** + * @return the number of frames in each data callback or kUnspecified. + */ + int32_t getFramesPerDataCallback() const { return mFramesPerCallback; } + + /** + * @return the audio sample format (e.g. Float or I16) + */ + AudioFormat getFormat() const { return mFormat; } + + /** + * Query the maximum number of frames that can be filled without blocking. + * If the stream has been closed the last known value will be returned. + * + * @return buffer size + */ + virtual int32_t getBufferSizeInFrames() { return mBufferSizeInFrames; } + + /** + * @return capacityInFrames or kUnspecified + */ + virtual int32_t getBufferCapacityInFrames() const { return mBufferCapacityInFrames; } + + /** + * @return the sharing mode of the stream. + */ + SharingMode getSharingMode() const { return mSharingMode; } + + /** + * @return the performance mode of the stream. + */ + PerformanceMode getPerformanceMode() const { return mPerformanceMode; } + + /** + * @return the device ID of the stream. + */ + int32_t getDeviceId() const { + return mDeviceIds.empty() ? kUnspecified : mDeviceIds[0]; + } + + std::vector getDeviceIds() const { + return mDeviceIds; + } + + /** + * For internal use only. + * @return the data callback object for this stream, if set. + */ + AudioStreamDataCallback *getDataCallback() const { + return mDataCallback; + } + + /** + * For internal use only. + * @return the partial data callback object for this stream, if set. + */ + AudioStreamPartialDataCallback* getPartialDataCallback() const { + return mPartialDataCallback; + } + + /** + * For internal use only. + * @return the error callback object for this stream, if set. + */ + AudioStreamErrorCallback *getErrorCallback() const { + return mErrorCallback; + } + + /** + * For internal use only. + * @return the presentation callback object for this stream, if set. + */ + std::shared_ptr getPresentationCallback() const { + return mSharedPresentationCallback; + } + + /** + * @return true if a data callback was set for this stream + */ + bool isDataCallbackSpecified() const { + return mDataCallback != nullptr; + } + + /** + * @return true if a partial data callback was set for this stream + */ + bool isPartialDataCallbackSpecified() const { + return mPartialDataCallback != nullptr; + } + + /** + * @return true if a data callback or a partial data callback was set for this stream + */ + bool anyDataCallbackSpecified() const { + return isDataCallbackSpecified() || isPartialDataCallbackSpecified(); + } + + /** + * Note that if the app does not set an error callback then a + * default one may be provided. + * @return true if an error callback was set for this stream + */ + bool isErrorCallbackSpecified() const { + return mErrorCallback != nullptr; + } + + /** + * @return true if a presentation callback was set for this stream + */ + bool isPresentationCallbackSpecified() const { + return mSharedPresentationCallback != nullptr; + } + + /** + * @return the usage for this stream. + */ + Usage getUsage() const { return mUsage; } + + /** + * @return the stream's content type. + */ + ContentType getContentType() const { return mContentType; } + + /** + * @return the stream's input preset. + */ + InputPreset getInputPreset() const { return mInputPreset; } + + /** + * @return the stream's session ID allocation strategy (None or Allocate). + */ + SessionId getSessionId() const { return mSessionId; } + + /** + * @return whether the content of the stream is spatialized. + */ + bool isContentSpatialized() const { return mIsContentSpatialized; } + + /** + * @return the spatialization behavior for the stream. + */ + SpatializationBehavior getSpatializationBehavior() const { return mSpatializationBehavior; } + + /** + * Return the policy that determines whether the audio may or may not be captured + * by other apps or the system. + * + * See AudioStreamBuilder_setAllowedCapturePolicy(). + * + * Added in API level 29 to AAudio. + * + * @return the allowed capture policy, for example AllowedCapturePolicy::All + */ + AllowedCapturePolicy getAllowedCapturePolicy() const { return mAllowedCapturePolicy; } + + /** + * Return whether this input stream is marked as privacy sensitive. + * + * See AudioStreamBuilder_setPrivacySensitiveMode(). + * + * Added in API level 30 to AAudio. + * + * @return PrivacySensitiveMode::Enabled if privacy sensitive, + * PrivacySensitiveMode::Disabled if not privacy sensitive, and + * PrivacySensitiveMode::Unspecified if API is not supported. + */ + PrivacySensitiveMode getPrivacySensitiveMode() const { return mPrivacySensitiveMode; } + + /** + * Return the stream's package name + * + * See AudioStreamBuilder_setPackageName(). + * + * Added in API level 31 to AAudio. + * + * @return packageName + */ + std::string getPackageName() const { return mPackageName; } + + /** + * Return the stream's attribution tag + * + * See AudioStreamBuilder_setAttributionTag(). + * + * Added in API level 31 to AAudio. + * + * @return attributionTag + */ + std::string getAttributionTag() const { return mAttributionTag; } + + /** + * @return true if Oboe can convert channel counts to achieve optimal results. + */ + bool isChannelConversionAllowed() const { + return mChannelConversionAllowed; + } + + /** + * @return true if Oboe can convert data formats to achieve optimal results. + */ + bool isFormatConversionAllowed() const { + return mFormatConversionAllowed; + } + + /** + * @return whether and how Oboe can convert sample rates to achieve optimal results. + */ + SampleRateConversionQuality getSampleRateConversionQuality() const { + return mSampleRateConversionQuality; + } + + /** + * @return the stream's channel mask. + */ + ChannelMask getChannelMask() const { + return mChannelMask; + } + + /** + * @return number of channels for the hardware, for example 2 for stereo, or kUnspecified. + */ + int32_t getHardwareChannelCount() const { return mHardwareChannelCount; } + + /** + * @return hardware sample rate for the stream or kUnspecified + */ + int32_t getHardwareSampleRate() const { return mHardwareSampleRate; } + + /** + * @return the audio sample format of the hardware (e.g. Float or I16) + */ + AudioFormat getHardwareFormat() const { return mHardwareFormat; } + +protected: + /** The callback which will be fired when new data is ready to be read/written. **/ + AudioStreamDataCallback *mDataCallback = nullptr; + std::shared_ptr mSharedDataCallback; + + /** The partial data callback which will be fired when new data is ready to be read/written. **/ + AudioStreamPartialDataCallback *mPartialDataCallback = nullptr; + std::shared_ptr mSharedPartialDataCallback; + + /** The callback which will be fired when an error or a disconnect occurs. **/ + AudioStreamErrorCallback *mErrorCallback = nullptr; + std::shared_ptr mSharedErrorCallback; + + std::shared_ptr mSharedPresentationCallback; + + /** Number of audio frames which will be requested in each callback */ + int32_t mFramesPerCallback = kUnspecified; + /** Stream channel count */ + int32_t mChannelCount = kUnspecified; + /** Stream sample rate */ + int32_t mSampleRate = kUnspecified; + /** Stream buffer capacity specified as a number of audio frames */ + int32_t mBufferCapacityInFrames = kUnspecified; + /** Stream buffer size specified as a number of audio frames */ + int32_t mBufferSizeInFrames = kUnspecified; + /** Stream channel mask. Only active on Android 32+ */ + ChannelMask mChannelMask = ChannelMask::Unspecified; + + /** Stream sharing mode */ + SharingMode mSharingMode = SharingMode::Shared; + /** Format of audio frames */ + AudioFormat mFormat = AudioFormat::Unspecified; + /** Stream direction */ + Direction mDirection = Direction::Output; + /** Stream performance mode */ + PerformanceMode mPerformanceMode = PerformanceMode::None; + + /** Stream usage. Only active on Android 28+ */ + Usage mUsage = Usage::Media; + /** Stream content type. Only active on Android 28+ */ + ContentType mContentType = ContentType::Music; + /** Stream input preset. Only active on Android 28+ + * TODO InputPreset::Unspecified should be considered as a possible default alternative. + */ + InputPreset mInputPreset = InputPreset::VoiceRecognition; + /** Stream session ID allocation strategy. Only active on Android 28+ */ + SessionId mSessionId = SessionId::None; + + /** Allowed Capture Policy. Only active on Android 29+ */ + AllowedCapturePolicy mAllowedCapturePolicy = AllowedCapturePolicy::Unspecified; + + /** Privacy Sensitive Mode. Only active on Android 30+ */ + PrivacySensitiveMode mPrivacySensitiveMode = PrivacySensitiveMode::Unspecified; + + /** Control the name of the package creating the stream. Only active on Android 31+ */ + std::string mPackageName; + /** Control the attribution tag of the context creating the stream. Only active on Android 31+ */ + std::string mAttributionTag; + + /** Whether the content is already spatialized. Only used on Android 32+ */ + bool mIsContentSpatialized = false; + /** Spatialization Behavior. Only active on Android 32+ */ + SpatializationBehavior mSpatializationBehavior = SpatializationBehavior::Unspecified; + + /** Hardware channel count. Only specified on Android 34+ AAudio streams */ + int32_t mHardwareChannelCount = kUnspecified; + /** Hardware sample rate. Only specified on Android 34+ AAudio streams */ + int32_t mHardwareSampleRate = kUnspecified; + /** Hardware format. Only specified on Android 34+ AAudio streams */ + AudioFormat mHardwareFormat = AudioFormat::Unspecified; + + // Control whether Oboe can convert channel counts to achieve optimal results. + bool mChannelConversionAllowed = false; + // Control whether Oboe can convert data formats to achieve optimal results. + bool mFormatConversionAllowed = false; + // Control whether and how Oboe can convert sample rates to achieve optimal results. + SampleRateConversionQuality mSampleRateConversionQuality = SampleRateConversionQuality::Medium; + + std::vector mDeviceIds; + + /** Validate stream parameters that might not be checked in lower layers */ + virtual Result isValidConfig() { + switch (mFormat) { + case AudioFormat::Unspecified: + case AudioFormat::I16: + case AudioFormat::Float: + case AudioFormat::I24: + case AudioFormat::I32: + case AudioFormat::IEC61937: + case AudioFormat::MP3: + case AudioFormat::AAC_LC: + case AudioFormat::AAC_HE_V1: + case AudioFormat::AAC_HE_V2: + case AudioFormat::AAC_ELD: + case AudioFormat::AAC_XHE: + case AudioFormat::OPUS: + break; + + default: + return Result::ErrorInvalidFormat; + } + + switch (mSampleRateConversionQuality) { + case SampleRateConversionQuality::None: + case SampleRateConversionQuality::Fastest: + case SampleRateConversionQuality::Low: + case SampleRateConversionQuality::Medium: + case SampleRateConversionQuality::High: + case SampleRateConversionQuality::Best: + return Result::OK; + default: + return Result::ErrorIllegalArgument; + } + } +}; + +} // namespace oboe + +#endif /* OBOE_STREAM_BASE_H_ */ diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_AudioStreamBuilder_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_AudioStreamBuilder_android.h new file mode 100644 index 0000000..6421b05 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_AudioStreamBuilder_android.h @@ -0,0 +1,782 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_STREAM_BUILDER_H_ +#define OBOE_STREAM_BUILDER_H_ + +#include "oboe_oboe_Definitions_android.h" +#include "oboe_oboe_AudioStreamBase_android.h" +#include "oboe_oboe_Utilities_android.h" +#include "oboe_oboe_ResultWithValue_android.h" + +namespace oboe { + + // This depends on AudioStream, so we use forward declaration, it will close and delete the stream + struct StreamDeleterFunctor; + using ManagedStream = std::unique_ptr; + +/** + * Factory class for an audio Stream. + */ +class AudioStreamBuilder : public AudioStreamBase { +public: + + AudioStreamBuilder() : AudioStreamBase() {} + + AudioStreamBuilder(const AudioStreamBase &audioStreamBase): AudioStreamBase(audioStreamBase) {} + + /** + * Request a specific number of channels. + * + * Default is kUnspecified. If the value is unspecified then + * the application should query for the actual value after the stream is opened. + * + * As the channel count here may be different from the corresponding channel count of + * provided channel mask used in setChannelMask(). The last called will be respected + * if this function and setChannelMask() are called. + */ + AudioStreamBuilder *setChannelCount(int channelCount) { + mChannelCount = channelCount; + mChannelMask = ChannelMask::Unspecified; + return this; + } + + /** + * Request a specific channel mask. + * + * Default is kUnspecified. If the value is unspecified then the application + * should query for the actual value after the stream is opened. + * + * As the corresponding channel count of provided channel mask here may be different + * from the channel count used in setChannelCount(). The last called will be respected + * if this function and setChannelCount() are called. + * + * As the setChannelMask API is available on Android 32+, this call will only take effects + * on Android 32+. + */ + AudioStreamBuilder *setChannelMask(ChannelMask channelMask) { + mChannelMask = channelMask; + mChannelCount = getChannelCountFromChannelMask(channelMask); + return this; + } + + /** + * Request the direction for a stream. The default is Direction::Output. + * + * @param direction Direction::Output or Direction::Input + */ + AudioStreamBuilder *setDirection(Direction direction) { + mDirection = direction; + return this; + } + + /** + * Request a specific sample rate in Hz. + * + * Default is kUnspecified. If the value is unspecified then + * the application should query for the actual value after the stream is opened. + * + * Technically, this should be called the "frame rate" or "frames per second", + * because it refers to the number of complete frames transferred per second. + * But it is traditionally called "sample rate". Se we use that term. + * + */ + AudioStreamBuilder *setSampleRate(int32_t sampleRate) { + mSampleRate = sampleRate; + return this; + } + + /** + * @deprecated use `setFramesPerDataCallback` instead. + */ + AudioStreamBuilder *setFramesPerCallback(int framesPerCallback) { + return setFramesPerDataCallback(framesPerCallback); + } + + /** + * Request a specific number of frames for the data callback. + * + * Default is kUnspecified. If the value is unspecified then + * the actual number may vary from callback to callback. + * + * If an application can handle a varying number of frames then we recommend + * leaving this unspecified. This allow the underlying API to optimize + * the callbacks. But if your application is, for example, doing FFTs or other block + * oriented operations, then call this function to get the sizes you need. + * + * Calling setFramesPerDataCallback() does not guarantee anything about timing. + * This just collects the data into a the number of frames that your app requires. + * We encourage leaving this unspecified in most cases. + * + * If this number is larger than the burst size, some bursts will not receive a callback. + * If this number is smaller than the burst size, there may be multiple callbacks in a single + * burst. + * + * @param framesPerCallback + * @return pointer to the builder so calls can be chained + */ + AudioStreamBuilder *setFramesPerDataCallback(int framesPerCallback) { + mFramesPerCallback = framesPerCallback; + return this; + } + + /** + * Request a sample data format, for example Format::Float. + * + * Default is Format::Unspecified. If the value is unspecified then + * the application should query for the actual value after the stream is opened. + */ + AudioStreamBuilder *setFormat(AudioFormat format) { + mFormat = format; + return this; + } + + /** + * Set the requested buffer capacity in frames. + * BufferCapacityInFrames is the maximum possible BufferSizeInFrames. + * + * The final stream capacity may differ. For AAudio it should be at least this big. + * For OpenSL ES, it could be smaller. + * + * Default is kUnspecified. + * + * @param bufferCapacityInFrames the desired buffer capacity in frames or kUnspecified + * @return pointer to the builder so calls can be chained + */ + AudioStreamBuilder *setBufferCapacityInFrames(int32_t bufferCapacityInFrames) { + mBufferCapacityInFrames = bufferCapacityInFrames; + return this; + } + + /** + * Get the audio API which will be requested when opening the stream. No guarantees that this is + * the API which will actually be used. Query the stream itself to find out the API which is + * being used. + * + * If you do not specify the API, then AAudio will be used if isAAudioRecommended() + * returns true. Otherwise OpenSL ES will be used. + * + * @return the requested audio API + */ + AudioApi getAudioApi() const { return mAudioApi; } + + /** + * If you leave this unspecified then Oboe will choose the best API + * for the device and SDK version at runtime. + * + * This should almost always be left unspecified, except for debugging purposes. + * Specifying AAudio will force Oboe to use AAudio on 8.0, which is extremely risky. + * Specifying OpenSLES should mainly be used to test legacy performance/functionality. + * + * If the caller requests AAudio and it is supported then AAudio will be used. + * + * @param audioApi Must be AudioApi::Unspecified, AudioApi::OpenSLES or AudioApi::AAudio. + * @return pointer to the builder so calls can be chained + */ + AudioStreamBuilder *setAudioApi(AudioApi audioApi) { + mAudioApi = audioApi; + return this; + } + + /** + * Is the AAudio API supported on this device? + * + * AAudio was introduced in the Oreo 8.0 release. + * + * @return true if supported + */ + static bool isAAudioSupported(); + + /** + * Is the AAudio API recommended this device? + * + * AAudio may be supported but not recommended because of version specific issues. + * AAudio is not recommended for Android 8.0 or earlier versions. + * + * @return true if recommended + */ + static bool isAAudioRecommended(); + + /** + * Request a mode for sharing the device. + * The requested sharing mode may not be available. + * So the application should query for the actual mode after the stream is opened. + * + * @param sharingMode SharingMode::Shared or SharingMode::Exclusive + * @return pointer to the builder so calls can be chained + */ + AudioStreamBuilder *setSharingMode(SharingMode sharingMode) { + mSharingMode = sharingMode; + return this; + } + + /** + * Request a performance level for the stream. + * This will determine the latency, the power consumption, and the level of + * protection from glitches. + * + * @param performanceMode for example, PerformanceMode::LowLatency + * @return pointer to the builder so calls can be chained + */ + AudioStreamBuilder *setPerformanceMode(PerformanceMode performanceMode) { + mPerformanceMode = performanceMode; + return this; + } + + + /** + * Set the intended use case for an output stream. + * + * The system will use this information to optimize the behavior of the stream. + * This could, for example, affect how volume and focus is handled for the stream. + * The usage is ignored for input streams. + * + * The default, if you do not call this function, is Usage::Media. + * + * Added in API level 28. + * + * @param usage the desired usage, eg. Usage::Game + */ + AudioStreamBuilder *setUsage(Usage usage) { + mUsage = usage; + return this; + } + + /** + * Set the type of audio data that an output stream will carry. + * + * The system will use this information to optimize the behavior of the stream. + * This could, for example, affect whether a stream is paused when a notification occurs. + * The contentType is ignored for input streams. + * + * The default, if you do not call this function, is ContentType::Music. + * + * Added in API level 28. + * + * @param contentType the type of audio data, eg. ContentType::Speech + */ + AudioStreamBuilder *setContentType(ContentType contentType) { + mContentType = contentType; + return this; + } + + /** + * Set the input (capture) preset for the stream. + * + * The system will use this information to optimize the behavior of the stream. + * This could, for example, affect which microphones are used and how the + * recorded data is processed. + * + * The default, if you do not call this function, is InputPreset::VoiceRecognition. + * That is because VoiceRecognition is the preset with the lowest latency + * on many platforms. + * + * Added in API level 28. + * + * @param inputPreset the desired configuration for recording + */ + AudioStreamBuilder *setInputPreset(InputPreset inputPreset) { + mInputPreset = inputPreset; + return this; + } + + /** Set the requested session ID. + * + * The session ID can be used to associate a stream with effects processors. + * The effects are controlled using the Android AudioEffect Java API. + * + * The default, if you do not call this function, is SessionId::None. + * + * If set to SessionId::Allocate then a session ID will be allocated + * when the stream is opened. + * + * The allocated session ID can be obtained by calling AudioStream::getSessionId() + * and then used with this function when opening another stream. + * This allows effects to be shared between streams. + * + * Session IDs from Oboe can be used the Android Java APIs and vice versa. + * So a session ID from an Oboe stream can be passed to Java + * and effects applied using the Java AudioEffect API. + * + * Allocated session IDs will always be positive and nonzero. + * + * Added in API level 28. + * + * @param sessionId an allocated sessionID or SessionId::Allocate + */ + AudioStreamBuilder *setSessionId(SessionId sessionId) { + mSessionId = sessionId; + return this; + } + + /** + * Request a stream to a specific audio input/output device given an audio device ID. + * + * In most cases, the primary device will be the appropriate device to use, and the + * deviceId can be left kUnspecified. + * + * The ID could be obtained from the Java AudioManager. + * AudioManager.getDevices() returns an array of AudioDeviceInfo, + * which contains a getId() method. That ID can be passed to this function. + * + * It is possible that you may not get the device that you requested. + * So if it is important to you, you should call + * stream->getDeviceId() after the stream is opened to + * verify the actual ID. + * + * Note that when using OpenSL ES, this will be ignored and the created + * stream will have deviceId kUnspecified. + * + * @param deviceId device identifier or kUnspecified + * @return pointer to the builder so calls can be chained + */ + AudioStreamBuilder *setDeviceId(int32_t deviceId) { + mDeviceIds.clear(); + if (deviceId != kUnspecified) { + mDeviceIds.push_back(deviceId); + } + return this; + } + + /** + * Specify whether this stream audio may or may not be captured by other apps or the system. + * + * The default is AllowedCapturePolicy::Unspecified which maps to AAUDIO_ALLOW_CAPTURE_BY_ALL. + * + * Note that an application can also set its global policy, in which case the most restrictive + * policy is always applied. See android.media.AudioAttributes.setAllowedCapturePolicy. + * + * Added in API level 29 to AAudio. + * + * @param inputPreset the desired level of opt-out from being captured. + * @return pointer to the builder so calls can be chained + */ + AudioStreamBuilder *setAllowedCapturePolicy(AllowedCapturePolicy allowedCapturePolicy) { + mAllowedCapturePolicy = allowedCapturePolicy; + return this; + } + + /** Indicates whether this input stream must be marked as privacy sensitive or not. + * + * When PrivacySensitiveMode::Enabled, this input stream is privacy sensitive and any + * concurrent capture is not permitted. + * + * This is off (PrivacySensitiveMode::Disabled) by default except when the input preset is + * InputPreset::VoiceRecognition or InputPreset::Camcorder + * + * Always takes precedence over default from input preset when set explicitly. + * + * Only relevant if the stream direction is Direction::Input and AAudio is used. + * + * Added in API level 30 to AAudio. + * + * @param privacySensitive PrivacySensitiveMode::Enabled if capture from this stream must be + * marked as privacy sensitive, PrivacySensitiveMode::Disabled if stream should be marked as + * not sensitive. + * @return pointer to the builder so calls can be chained + */ + AudioStreamBuilder *setPrivacySensitiveMode(PrivacySensitiveMode privacySensitiveMode) { + mPrivacySensitiveMode = privacySensitiveMode; + return this; + } + + /** + * Specifies whether the audio data of this output stream has already been processed for spatialization. + * + * If the stream has been processed for spatialization, setting this to true will prevent issues such as + * double-processing on platforms that will spatialize audio data. + * + * This is false by default. + * + * Available since API level 32. + * + * @param isContentSpatialized whether the content is already spatialized + * @return pointer to the builder so calls can be chained + */ + AudioStreamBuilder *setIsContentSpatialized(bool isContentSpatialized) { + mIsContentSpatialized = isContentSpatialized; + return this; + } + + /** + * Sets the behavior affecting whether spatialization will be used. + * + * The AAudio system will use this information to select whether the stream will go through a + * spatializer effect or not when the effect is supported and enabled. + * + * This is SpatializationBehavior::Never by default. + * + * Available since API level 32. + * + * @param spatializationBehavior the desired spatialization behavior + * @return pointer to the builder so calls can be chained + */ + AudioStreamBuilder *setSpatializationBehavior(SpatializationBehavior spatializationBehavior) { + mSpatializationBehavior = spatializationBehavior; + return this; + } + + /** + * Specifies an object to handle data related callbacks from the underlying API. + * + * Important: See AudioStreamCallback for restrictions on what may be called + * from the callback methods. + * + * We pass a shared_ptr so that the sharedDataCallback object cannot be deleted + * before the stream is deleted. + * + * If both this method and setPartialDataCallback(std::shared_ptr) + * are called, the data callback from the last called method will be used. + * + * Note that if the stream is offloaded or compress formats, it is suggested to use + * setPartialDataCallback(std::shared_ptr) when it is available. + * The reason is that AudioStreamDataCallback will require apps to process all the provided + * data or none of the provided data. This is not suitable for compressed audio data, for + * gapless audio playback, or to drain audio to a fixed arbitrary stop point in frames. + * + * @param sharedDataCallback + * @return pointer to the builder so calls can be chained + */ + AudioStreamBuilder *setDataCallback( + std::shared_ptr sharedDataCallback) { + // Use this raw pointer in the rest of the code to retain backwards compatibility. + mDataCallback = sharedDataCallback.get(); + // Hold a shared_ptr to protect the raw pointer for the lifetime of the stream. + mSharedDataCallback = sharedDataCallback; + mSharedPartialDataCallback.reset(); + mPartialDataCallback = nullptr; + return this; + } + + /** + * Pass a raw pointer to a data callback. This is not recommended because the dataCallback + * object might get deleted by the app while it is being used. + * + * If both this method and setPartialDataCallback(std::shared_ptr) + * are called, the data callback from the last called method will be used. + * + * Note that if the stream is offloaded or compress formats, it is suggested to use + * setPartialDataCallback(std::shared_ptr) when it is available. + * The reason is that AudioStreamDataCallback will require apps to process all the provided + * data or none of the provided data. This is not suitable for compressed audio data, for + * gapless audio playback, or to drain audio to a fixed arbitrary stop point in frames. + * + * @deprecated Call setDataCallback(std::shared_ptr) instead. + * @param dataCallback + * @return pointer to the builder so calls can be chained + */ + AudioStreamBuilder *setDataCallback(AudioStreamDataCallback *dataCallback) { + mDataCallback = dataCallback; + mSharedDataCallback = nullptr; + mPartialDataCallback = nullptr; + mSharedPartialDataCallback.reset(); + return this; + } + + /** + * Specifies an object to handle data related callbacks from the underlying API. + * + * Important: See AudioStreamPartialDataCallback for restrictions on what may be called + * from the callback methods. + * + * We pass a shared_ptr and cache it so that the partial data callback object cannot be deleted + * before the stream is deleted. + * + * If both this method and setDataCallback(AudioStreamDataCallback*) or + * setDataCallback(std::shared_ptr) are called, + * the data callback from the last called method will be used. + * + * Note that partial data callback from aaudio API at API level 37. In that case, when partial + * data callback is set on the Android device that is not supporting partial data callback API, + * the stream will fail to open. + * + * Also note that partial data callback is only supported by aaudio API. OpenSLES has been + * deprecated for years. When setting parital data callback and using openSLES will result in + * failing to open. + * + * When the stream is in low latency mode, the data buffer is pretty small. In that case, it + * may be easier to use AudioStreamDataCallback instead of AudioStreamPartialDataCallback. For + * other use cases that use a big buffer, such as offload playback, deep buffer playback, it + * will make more sense to use partial data callback. When the stream is offloaded, no data + * conversion is allowed. When the stream is a deep buffer stream, the data conversion will be + * provided by the Android framework. In that case, partial data callback is currently only + * supported without using data conversion from oboe. + * + * Call OboeExtensions::isPartialDataCallbackSupported() to check if partial data + * callback is supported by the device or not. + * + * @param partialDataCallback + * @return pointer to the builder so calls can be chained + */ + AudioStreamBuilder *setPartialDataCallback( + const std::shared_ptr& partialDataCallback) { + mSharedDataCallback.reset(); + mDataCallback = nullptr; + mSharedPartialDataCallback = partialDataCallback; + mPartialDataCallback = mSharedPartialDataCallback.get(); + return this; + } + + /** + * Specifies an object to handle error related callbacks from the underlying API. + * This can occur when a stream is disconnected because a headset is plugged in or unplugged. + * It can also occur if the audio service fails or if an exclusive stream is stolen by + * another stream. + * + * Note that error callbacks will only be called when a data callback has been specified + * and the stream is started. If you are not using a data callback then the read(), write() + * and requestStart() methods will return errors if the stream is disconnected. + * + * Important: See AudioStreamCallback for restrictions on what may be called + * from the callback methods. + * + * When an error callback occurs, the associated stream must be stopped and closed + * in a separate thread. + * + * We pass a shared_ptr so that the errorCallback object cannot be deleted before the stream is deleted. + * If the stream was created using a shared_ptr then the stream cannot be deleted before the + * error callback has finished running. + * + * @param sharedErrorCallback + * @return pointer to the builder so calls can be chained + */ + AudioStreamBuilder *setErrorCallback(std::shared_ptr sharedErrorCallback) { + // Use this raw pointer in the rest of the code to retain backwards compatibility. + mErrorCallback = sharedErrorCallback.get(); + // Hold a shared_ptr to protect the raw pointer for the lifetime of the stream. + mSharedErrorCallback = sharedErrorCallback; + return this; + } + + /** + * Pass a raw pointer to an error callback. This is not recommended because the errorCallback + * object might get deleted by the app while it is being used. + * + * @deprecated Call setErrorCallback(std::shared_ptr) instead. + * @param errorCallback + * @return pointer to the builder so calls can be chained + */ + AudioStreamBuilder *setErrorCallback(AudioStreamErrorCallback *errorCallback) { + mErrorCallback = errorCallback; + mSharedErrorCallback = nullptr; + return this; + } + + /** + * Specifies an object to handle data presentation related callbacks from the underlying API. + * This can occur when all data queued in the audio system for an offload stream has been + * played. + * + * Note that presentation callbacks will only be called when a data callback has been specified + * and the stream is started. + * + * Important: See AudioStreamCallback for restrictions on what may be called + * from the callback methods. + * + * We pass a shared_ptr so that the presentationCallback object cannot be deleted before the + * stream is deleted. If the stream was created using a shared_ptr then the stream cannot be + * deleted before the presentation callback has finished running. + * + * @param sharedPresentationCallback + * @return pointer to the builder so calls can be chained + */ + AudioStreamBuilder *setPresentationCallback( + std::shared_ptr sharedPresentationCallback) { + mSharedPresentationCallback = sharedPresentationCallback; + return this; + } + + /** + * Specifies an object to handle data or error related callbacks from the underlying API. + * + * This is the equivalent of calling both setDataCallback() and setErrorCallback(). + * + * Important: See AudioStreamCallback for restrictions on what may be called + * from the callback methods. + * + * Note that when this is called, partial data callback will be reset. + * + * @deprecated Call setDataCallback(std::shared_ptr) and + * setErrorCallback(std::shared_ptr) instead. + * @param streamCallback + * @return pointer to the builder so calls can be chained + */ + AudioStreamBuilder *setCallback(AudioStreamCallback *streamCallback) { + // Use the same callback object for both, dual inheritance. + mDataCallback = streamCallback; + mErrorCallback = streamCallback; + mSharedPartialDataCallback.reset(); + mPartialDataCallback = nullptr; + return this; + } + + /** + * If true then Oboe might convert channel counts to achieve optimal results. + * On some versions of Android for example, stereo streams could not use a FAST track. + * So a mono stream might be used instead and duplicated to two channels. + * On some devices, mono streams might be broken, so a stereo stream might be opened + * and converted to mono. + * + * Default is false. + */ + AudioStreamBuilder *setChannelConversionAllowed(bool allowed) { + mChannelConversionAllowed = allowed; + return this; + } + + /** + * If true then Oboe might convert data formats to achieve optimal results. + * On some versions of Android, for example, a float stream could not get a + * low latency data path. So an I16 stream might be opened and converted to float. + * + * Default is false. + */ + AudioStreamBuilder *setFormatConversionAllowed(bool allowed) { + mFormatConversionAllowed = allowed; + return this; + } + + /** + * Specify the quality of the sample rate converter in Oboe. + * + * If set to None then Oboe will not do sample rate conversion. But the underlying APIs might + * still do sample rate conversion if you specify a sample rate. + * That can prevent you from getting a low latency stream. + * + * If you do the conversion in Oboe then you might still get a low latency stream. + * + * Default is SampleRateConversionQuality::Medium + */ + AudioStreamBuilder *setSampleRateConversionQuality(SampleRateConversionQuality quality) { + mSampleRateConversionQuality = quality; + return this; + } + + /** + * Declare the name of the package creating the stream. + * + * This is usually {@code Context#getPackageName()}. + * + * The default, if you do not call this function, is a random package in the calling uid. + * The vast majority of apps have only one package per calling UID. + * If an invalid package name is set, input streams may not be given permission to + * record when started. + * + * Please declare this as some input streams will fail permission checks otherwise. + * + * The package name is usually the applicationId in your app's build.gradle file. + * + * Available since API level 31. + * + * @param packageName packageName of the calling app. + */ + AudioStreamBuilder *setPackageName(std::string packageName) { + mPackageName = packageName; + return this; + } + + /** + * Declare the attribution tag of the context creating the stream. + * + * This is usually {@code Context#getAttributionTag()}. + * + * The default, if you do not call this function, is null. + * + * Available since API level 31. + * + * @param attributionTag attributionTag of the calling context. + */ + AudioStreamBuilder *setAttributionTag(std::string attributionTag) { + mAttributionTag = attributionTag; + return this; + } + + /** + * @return true if AAudio will be used based on the current settings. + */ + bool willUseAAudio() const { + return (mAudioApi == AudioApi::AAudio && isAAudioSupported()) + || (mAudioApi == AudioApi::Unspecified && isAAudioRecommended()); + } + + /** + * Create and open a stream object based on the current settings. + * + * The caller owns the pointer to the AudioStream object + * and must delete it when finished. + * + * @deprecated Use openStream(std::shared_ptr &stream) instead. + * @param stream pointer to a variable to receive the stream address + * @return OBOE_OK if successful or a negative error code + */ + Result openStream(AudioStream **stream); + + /** + * Create and open a stream object based on the current settings. + * + * The caller shares the pointer to the AudioStream object. + * The shared_ptr is used internally by Oboe to prevent the stream from being + * deleted while it is being used by callbacks. + * + * @param stream reference to a shared_ptr to receive the stream address + * @return OBOE_OK if successful or a negative error code + */ + Result openStream(std::shared_ptr &stream); + + /** + * Create and open a ManagedStream object based on the current builder state. + * + * The caller must create a unique ptr, and pass by reference so it can be + * modified to point to an opened stream. The caller owns the unique ptr, + * and it will be automatically closed and deleted when going out of scope. + * + * @deprecated Use openStream(std::shared_ptr &stream) instead. + * @param stream Reference to the ManagedStream (uniqueptr) used to keep track of stream + * @return OBOE_OK if successful or a negative error code. + */ + Result openManagedStream(ManagedStream &stream); + +private: + + /** + * Use this internally to implement opening with a shared_ptr. + * + * @param stream pointer to a variable to receive the stream address + * @return OBOE_OK if successful or a negative error code. + */ + Result openStreamInternal(AudioStream **streamPP); + + /** + * @param other + * @return true if channels, format and sample rate match + */ + bool isCompatible(AudioStreamBase &other); + + /** + * Create an AudioStream object. The AudioStream must be opened before use. + * + * The caller owns the pointer. + * + * @return pointer to an AudioStream object or nullptr. + */ + oboe::AudioStream *build(); + + AudioApi mAudioApi = AudioApi::Unspecified; +}; + +} // namespace oboe + +#endif /* OBOE_STREAM_BUILDER_H_ */ diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_AudioStreamCallback_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_AudioStreamCallback_android.h new file mode 100644 index 0000000..e5739cc --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_AudioStreamCallback_android.h @@ -0,0 +1,291 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_STREAM_CALLBACK_H +#define OBOE_STREAM_CALLBACK_H + +#include "oboe_oboe_Definitions_android.h" + +namespace oboe { + +class AudioStream; + +/** + * AudioStreamDataCallback defines a callback interface for + * moving data to/from an audio stream using `onAudioReady` + * 2) being alerted when a stream has an error using `onError*` methods + * + * It is used with AudioStreamBuilder::setDataCallback(). + */ + +class AudioStreamDataCallback { +public: + virtual ~AudioStreamDataCallback() = default; + + /** + * A buffer is ready for processing. + * + * For an output stream, this function should render and write numFrames of data + * in the stream's current data format to the audioData buffer. + * + * For an input stream, this function should read and process numFrames of data + * from the audioData buffer. + * + * The audio data is passed through the buffer. So do NOT call read() or + * write() on the stream that is making the callback. + * + * Note that numFrames can vary unless AudioStreamBuilder::setFramesPerCallback() + * is called. If AudioStreamBuilder::setFramesPerCallback() is NOT called then + * numFrames should always be <= AudioStream::getFramesPerBurst(). + * + * Also note that this callback function should be considered a "real-time" function. + * It must not do anything that could cause an unbounded delay because that can cause the + * audio to glitch or pop. + * + * These are things the function should NOT do: + *
    + *
  • allocate memory using, for example, malloc() or new
  • + *
  • any file operations such as opening, closing, reading or writing
  • + *
  • any network operations such as streaming
  • + *
  • use any mutexes or other synchronization primitives
  • + *
  • sleep
  • + *
  • oboeStream->stop(), pause(), flush() or close()
  • + *
  • oboeStream->read()
  • + *
  • oboeStream->write()
  • + *
+ * + * The following are OK to call from the data callback: + *
    + *
  • oboeStream->get*()
  • + *
  • oboe::convertToText()
  • + *
  • oboeStream->setBufferSizeInFrames()
  • + *
+ * + * If you need to move data, eg. MIDI commands, in or out of the callback function then + * we recommend the use of non-blocking techniques such as an atomic FIFO. + * + * @param audioStream pointer to the associated stream + * @param audioData buffer containing input data or a place to put output data + * @param numFrames number of frames to be processed + * @return DataCallbackResult::Continue or DataCallbackResult::Stop + */ + virtual DataCallbackResult onAudioReady( + AudioStream *audioStream, + void *audioData, + int32_t numFrames) = 0; +}; + +/** + * AudioStreamErrorCallback defines a callback interface for + * being alerted when a stream has an error or is disconnected + * using `onError*` methods. + * + * Note: This callback is only fired when an AudioStreamCallback is set. + * If you use AudioStream::write() you have to evaluate the return codes of + * AudioStream::write() to notice errors in the stream. + * + * It is used with AudioStreamBuilder::setErrorCallback(). + */ +class AudioStreamErrorCallback { +public: + virtual ~AudioStreamErrorCallback() = default; + + /** + * This will be called before other `onError` methods when an error occurs on a stream, + * such as when the stream is disconnected. + * + * It can be used to override and customize the normal error processing. + * Use of this method is considered an advanced technique. + * It might, for example, be used if an app want to use a high level lock when + * closing and reopening a stream. + * Or it might be used when an app want to signal a management thread that handles + * all of the stream state. + * + * If this method returns false it indicates that the stream has *not been stopped and closed + * by the application. In this case it will be stopped by Oboe in the following way: + * onErrorBeforeClose() will be called, then the stream will be closed and onErrorAfterClose() + * will be closed. + * + * If this method returns true it indicates that the stream *has* been stopped and closed + * by the application and Oboe will not do this. + * In that case, the app MUST stop() and close() the stream. + * + * This method will be called on a thread created by Oboe. + * + * @param audioStream pointer to the associated stream + * @param error + * @return true if the stream has been stopped and closed, false if not + */ + virtual bool onError(AudioStream* /* audioStream */, Result /* error */) { + return false; + } + + /** + * This will be called when an error occurs on a stream, + * such as when the stream is disconnected, + * and if onError() returns false (indicating that the error has not already been handled). + * + * Note that this will be called on a thread created by Oboe. + * + * The underlying stream will already be stopped by Oboe but not yet closed. + * So the stream can be queried. + * + * Do not close or delete the stream in this method because it will be + * closed after this method returns. + * + * @param audioStream pointer to the associated stream + * @param error + */ + virtual void onErrorBeforeClose(AudioStream* /* audioStream */, Result /* error */) {} + + /** + * This will be called when an error occurs on a stream, + * such as when the stream is disconnected, + * and if onError() returns false (indicating that the error has not already been handled). + * + * The underlying AAudio or OpenSL ES stream will already be stopped AND closed by Oboe. + * So the underlying stream cannot be referenced. + * But you can still query most parameters. + * + * This callback could be used to reopen a new stream on another device. + * + * @param audioStream pointer to the associated stream + * @param error + */ + virtual void onErrorAfterClose(AudioStream* /* audioStream */, Result /* error */) {} + +}; + +/** + * AudioStreamPresentationCallback defines a callback interface for + * being notified when a data presentation event is filed. + * + * It is used with AudioStreamBuilder::setPresentationCallback(). + */ +class AudioStreamPresentationCallback { +public: + virtual ~AudioStreamPresentationCallback() = default; + + /** + * This will be called when all the buffers of an offloaded + * stream that were queued in the audio system (e.g. the + * combination of the Android audio framework and the device's + * audio hardware) have been played. + * + * @param audioStream pointer to the associated stream + */ + virtual void onPresentationEnded(AudioStream* /* audioStream */) {} +}; + +/** + * AudioStreamPartialDataCallback defines a callback interface for + * moving data to/from an audio stream using `onAudioReady` + * + * It is used with AudioStreamBuilder::setPartialDataCallback(). The only difference + * between AudioStreamPartialDataCallback and AudioStreamDataCallback is that the + * AudioStreamPartialDataCallback allows apps to only process part of the requested + * data. + * + * Note that Android only support partial data callback from aaudio API at + * API level 37. In that case, when partial data callback is set on the Android + * device that is not supporting partial data callback API, apps must process all + * the requested data to avoid data lost. + * + * Call OboeExtensions::isPartialDataCallbackSupported() to check if partial data callback + * is supported by the device or not. + */ +class AudioStreamPartialDataCallback { +public: + virtual ~AudioStreamPartialDataCallback() = default; + + /** + * A buffer is ready for processing. If the client cannot process all the provided/requested + * data, returns a number to indicate the actual processed frames. + * + * For an output stream, this function should render and write at most numFrames of data + * in the stream's current data format to the audioData buffer. + * + * For an input stream, this function should read and process at most numFrames of data + * from the audioData buffer. + * + * The audio data is passed through the buffer. So do NOT call read() or + * write() on the stream that is making the callback. + * + * Note that numFrames can vary unless AudioStreamBuilder::setFramesPerCallback() + * is called. If AudioStreamBuilder::setFramesPerCallback() is NOT called then + * numFrames should always be <= AudioStream::getFramesPerBurst(). + * + * Also note that this callback function should be considered a "real-time" function. + * It must not do anything that could cause an unbounded delay because that can cause the + * audio to glitch or pop. + * + * These are things the function should NOT do: + *
    + *
  • allocate memory using, for example, malloc() or new
  • + *
  • any file operations such as opening, closing, reading or writing
  • + *
  • any network operations such as streaming
  • + *
  • use any mutexes or other synchronization primitives
  • + *
  • sleep
  • + *
  • oboeStream->stop(), pause(), flush() or close()
  • + *
  • oboeStream->read()
  • + *
  • oboeStream->write()
  • + *
+ * + * The following are OK to call from the data callback: + *
    + *
  • oboeStream->get*()
  • + *
  • oboe::convertToText()
  • + *
  • oboeStream->setBufferSizeInFrames()
  • + *
+ * + * If you need to move data, eg. MIDI commands, in or out of the callback function then + * we recommend the use of non-blocking techniques such as an atomic FIFO. + * + * @param audioStream pointer to the associated stream + * @param audioData buffer containing input data or a place to put output data + * @param numFrames number of frames to be processed + * @return the actual processed data frames, must be within range of [0, numFrames]. + * Returning a negative number will stop the stream. + */ + virtual int32_t onPartialAudioReady( + AudioStream *audioStream, + void *audioData, + int32_t numFrames) = 0; +}; + +/** + * AudioStreamCallback defines a callback interface for: + * + * 1) moving data to/from an audio stream using `onAudioReady` + * 2) being alerted when a stream has an error using `onError*` methods + * + * It is used with AudioStreamBuilder::setCallback(). + * + * It combines the interfaces defined by AudioStreamDataCallback and AudioStreamErrorCallback. + * This was the original callback object. We now recommend using the individual interfaces + * and using setDataCallback() and setErrorCallback(). + * + * @deprecated Use `AudioStreamDataCallback` and `AudioStreamErrorCallback` instead + */ +class AudioStreamCallback : public AudioStreamDataCallback, + public AudioStreamErrorCallback { +public: + virtual ~AudioStreamCallback() = default; +}; + +} // namespace oboe + +#endif //OBOE_STREAM_CALLBACK_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_AudioStream_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_AudioStream_android.h new file mode 100644 index 0000000..5168631 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_AudioStream_android.h @@ -0,0 +1,958 @@ +/* + * Copyright 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_STREAM_H_ +#define OBOE_STREAM_H_ + +#include +#include +#include +#include +#include "oboe_oboe_Definitions_android.h" +#include "oboe_oboe_ResultWithValue_android.h" +#include "oboe_oboe_AudioStreamBuilder_android.h" +#include "oboe_oboe_AudioStreamBase_android.h" +#include "oboe_oboe_Utilities_android.h" + +namespace oboe { + +/** + * The default number of nanoseconds to wait for when performing state change operations on the + * stream, such as `start` and `stop`. + * + * @see oboe::AudioStream::start + */ +constexpr int64_t kDefaultTimeoutNanos = (2000 * kNanosPerMillisecond); + +/** + * Base class for Oboe C++ audio stream. + */ +class AudioStream : public AudioStreamBase { + friend class AudioStreamBuilder; // allow access to setWeakThis() and lockWeakThis() +public: + + AudioStream() {} + + /** + * Construct an `AudioStream` using the given `AudioStreamBuilder` + * + * @param builder containing all the stream's attributes + */ + explicit AudioStream(const AudioStreamBuilder &builder); + + virtual ~AudioStream(); + + /** + * Open a stream based on the current settings. + * + * Note that we do not recommend re-opening a stream that has been closed. + * TODO Should we prevent re-opening? + * + * @return + */ + virtual Result open() { + return Result::OK; // Called by subclasses. Might do more in the future. + } + + /** + * Free the audio resources associated with a stream created by AAudioStreamBuilder_openStream(). + * + * AAudioStream_close() should be called at some point after calling this function. + * + * After this call, the stream will be in AAUDIO_STREAM_STATE_CLOSING + * + * This function is useful if you want to release the audio resources immediately, but still allow + * queries to the stream to occur from other threads. This often happens if you are monitoring + * stream progress from a UI thread. + * + * NOTE: This function is only fully implemented for MMAP streams, which are low latency streams + * supported by some devices. On other "Legacy" streams some audio resources will still be in use + * and some callbacks may still be in process after this call. + * + * Available in AAudio since API level 30. Returns Result::ErrorUnimplemented otherwise. + * + * * @return either Result::OK or an error. + */ + virtual Result release() { + return Result::ErrorUnimplemented; + } + + /** + * Close the stream and deallocate any resources from the open() call. + */ + virtual Result close(); + + /** + * Start the stream. This will block until the stream has been started, an error occurs + * or `timeoutNanoseconds` has been reached. + */ + virtual Result start(int64_t timeoutNanoseconds = kDefaultTimeoutNanos); + + /** + * Pause the stream. This will block until the stream has been paused, an error occurs + * or `timeoutNanoseconds` has been reached. + */ + virtual Result pause(int64_t timeoutNanoseconds = kDefaultTimeoutNanos); + + /** + * Flush the stream. This will block until the stream has been flushed, an error occurs + * or `timeoutNanoseconds` has been reached. + */ + virtual Result flush(int64_t timeoutNanoseconds = kDefaultTimeoutNanos); + + /** + * Stop the stream. This will block until the stream has been stopped, an error occurs + * or `timeoutNanoseconds` has been reached. + */ + virtual Result stop(int64_t timeoutNanoseconds = kDefaultTimeoutNanos); + + /* Asynchronous requests. + * Use waitForStateChange() if you need to wait for completion. + */ + + /** + * Start the stream asynchronously. Returns immediately (does not block). Equivalent to calling + * `start(0)`. + */ + virtual Result requestStart() = 0; + + /** + * Pause the stream asynchronously. Returns immediately (does not block). Equivalent to calling + * `pause(0)`. + */ + virtual Result requestPause() = 0; + + /** + * Flush the stream asynchronously. Returns immediately (does not block). Equivalent to calling + * `flush(0)`. + */ + virtual Result requestFlush() = 0; + + /** + * Stop the stream asynchronously. Returns immediately (does not block). Equivalent to calling + * `stop(0)`. + */ + virtual Result requestStop() = 0; + + /** + * Query the current state, eg. StreamState::Pausing + * + * @return state or a negative error. + */ + virtual StreamState getState() = 0; + + /** + * Wait until the stream's current state no longer matches the input state. + * The input state is passed to avoid race conditions caused by the state + * changing between calls. + * + * Note that generally applications do not need to call this. It is considered + * an advanced technique and is mostly used for testing. + * + *

+     * int64_t timeoutNanos = 500 * kNanosPerMillisecond; // arbitrary 1/2 second
+     * StreamState currentState = stream->getState();
+     * StreamState nextState = StreamState::Unknown;
+     * while (result == Result::OK && currentState != StreamState::Paused) {
+     *     result = stream->waitForStateChange(
+     *                                   currentState, &nextState, timeoutNanos);
+     *     currentState = nextState;
+     * }
+     * 
+ * + * If the state does not change within the timeout period then it will + * return ErrorTimeout. This is true even if timeoutNanoseconds is zero. + * + * @param inputState The state we want to change away from. + * @param nextState Pointer to a variable that will be set to the new state. + * @param timeoutNanoseconds The maximum time to wait in nanoseconds. + * @return Result::OK or a Result::Error. + */ + virtual Result waitForStateChange(StreamState inputState, + StreamState *nextState, + int64_t timeoutNanoseconds) = 0; + + /** + * This can be used to adjust the latency of the buffer by changing + * the threshold where blocking will occur. + * By combining this with getXRunCount(), the latency can be tuned + * at run-time for each device. + * + * This cannot be set higher than getBufferCapacity(). + * + * This should only be used with Output streams. It will + * be ignored for Input streams because they are generally kept as empty as possible. + * + * For OpenSL ES, this method only has an effect on output stream that do NOT + * use a callback. The blocking writes goes into a buffer in Oboe and the size of that + * buffer is controlled by this method. + * + * @param requestedFrames requested number of frames that can be filled without blocking + * @return the resulting buffer size in frames (obtained using value()) or an error (obtained + * using error()) + */ + virtual ResultWithValue setBufferSizeInFrames(int32_t /* requestedFrames */) { + return Result::ErrorUnimplemented; + } + + /** + * An XRun is an Underrun or an Overrun. + * During playing, an underrun will occur if the stream is not written in time + * and the system runs out of valid data. + * During recording, an overrun will occur if the stream is not read in time + * and there is no place to put the incoming data so it is discarded. + * + * An underrun or overrun can cause an audible "pop" or "glitch". + * + * @return a result which is either Result::OK with the xRun count as the value, or a + * Result::Error* code + */ + virtual ResultWithValue getXRunCount() { + return ResultWithValue(Result::ErrorUnimplemented); + } + + /** + * @return true if XRun counts are supported on the stream + */ + virtual bool isXRunCountSupported() const = 0; + + /** + * Query the number of frames that are read or written by the endpoint at one time. + * + * @return burst size + */ + int32_t getFramesPerBurst() const { + return mFramesPerBurst; + } + + /** + * Get the number of bytes in each audio frame. This is calculated using the channel count + * and the sample format. For example, a 2 channel floating point stream will have + * 2 * 4 = 8 bytes per frame. + * + * Note for compressed formats, bytes per frames is treated as 1 by convention. + * + * @return number of bytes in each audio frame. + */ + int32_t getBytesPerFrame() const { + return isCompressedFormat(mFormat) ? 1 : mChannelCount * getBytesPerSample(); } + + /** + * Get the number of bytes per sample. This is calculated using the sample format. For example, + * a stream using 16-bit integer samples will have 2 bytes per sample. + * + * Note for compressed formats, they may not have a fixed bytes per sample. In that case, + * this method will return 0 for compressed format. + * + * @return the number of bytes per sample. + */ + int32_t getBytesPerSample() const; + + /** + * The number of audio frames written into the stream. + * This monotonic counter will never get reset. + * + * @return the number of frames written so far + */ + virtual int64_t getFramesWritten(); + + /** + * The number of audio frames read from the stream. + * This monotonic counter will never get reset. + * + * @return the number of frames read so far + */ + virtual int64_t getFramesRead(); + + /** + * Calculate the latency of a stream based on getTimestamp(). + * + * Output latency is the time it takes for a given frame to travel from the + * app to some type of digital-to-analog converter. If the DAC is external, for example + * in a USB interface or a TV connected by HDMI, then there may be additional latency + * that the Android device is unaware of. + * + * Input latency is the time it takes to a given frame to travel from an analog-to-digital + * converter (ADC) to the app. + * + * Note that the latency of an OUTPUT stream will increase abruptly when you write data to it + * and then decrease slowly over time as the data is consumed. + * + * The latency of an INPUT stream will decrease abruptly when you read data from it + * and then increase slowly over time as more data arrives. + * + * The latency of an OUTPUT stream is generally higher than the INPUT latency + * because an app generally tries to keep the OUTPUT buffer full and the INPUT buffer empty. + * + * Note that due to issues in Android before R, we recommend NOT calling + * this method from a data callback. See this tech note for more details. + * https://github.com/google/oboe/wiki/TechNote_ReleaseBuffer + * + * @return a ResultWithValue which has a result of Result::OK and a value containing the latency + * in milliseconds, or a result of Result::Error*. + */ + virtual ResultWithValue calculateLatencyMillis() { + return ResultWithValue(Result::ErrorUnimplemented); + } + + /** + * Get the estimated time that the frame at `framePosition` entered or left the audio processing + * pipeline. + * + * This can be used to coordinate events and interactions with the external environment, and to + * estimate the latency of an audio stream. An example of usage can be found in the hello-oboe + * sample (search for "calculateCurrentOutputLatencyMillis"). + * + * The time is based on the implementation's best effort, using whatever knowledge is available + * to the system, but cannot account for any delay unknown to the implementation. + * + * Note that due to issues in Android before R, we recommend NOT calling + * this method from a data callback. See this tech note for more details. + * https://github.com/google/oboe/wiki/TechNote_ReleaseBuffer + * + * @deprecated since 1.0, use AudioStream::getTimestamp(clockid_t clockId) instead, which + * returns ResultWithValue + * @param clockId the type of clock to use e.g. CLOCK_MONOTONIC + * @param framePosition the frame number to query + * @param timeNanoseconds an output parameter which will contain the presentation timestamp + */ + virtual Result getTimestamp(clockid_t /* clockId */, + int64_t* /* framePosition */, + int64_t* /* timeNanoseconds */) { + return Result::ErrorUnimplemented; + } + + /** + * Get the estimated time that the frame at `framePosition` entered or left the audio processing + * pipeline. + * + * This can be used to coordinate events and interactions with the external environment, and to + * estimate the latency of an audio stream. An example of usage can be found in the hello-oboe + * sample (search for "calculateCurrentOutputLatencyMillis"). + * + * The time is based on the implementation's best effort, using whatever knowledge is available + * to the system, but cannot account for any delay unknown to the implementation. + * + * Note that due to issues in Android before R, we recommend NOT calling + * this method from a data callback. See this tech note for more details. + * https://github.com/google/oboe/wiki/TechNote_ReleaseBuffer + * + * See + * @param clockId the type of clock to use e.g. CLOCK_MONOTONIC + * @return a FrameTimestamp containing the position and time at which a particular audio frame + * entered or left the audio processing pipeline, or an error if the operation failed. + */ + virtual ResultWithValue getTimestamp(clockid_t /* clockId */); + + // ============== I/O =========================== + /** + * Write data from the supplied buffer into the stream. This method will block until the write + * is complete or it runs out of time. + * + * If `timeoutNanoseconds` is zero then this call will not wait. + * + * @param buffer The address of the first sample. + * @param numFrames Number of frames to write. Only complete frames will be written. + * @param timeoutNanoseconds Maximum number of nanoseconds to wait for completion. + * @return a ResultWithValue which has a result of Result::OK and a value containing the number + * of frames actually written, or result of Result::Error*. + */ + virtual ResultWithValue write(const void* /* buffer */, + int32_t /* numFrames */, + int64_t /* timeoutNanoseconds */ ) { + return ResultWithValue(Result::ErrorUnimplemented); + } + + /** + * Read data into the supplied buffer from the stream. This method will block until the read + * is complete or it runs out of time. + * + * If `timeoutNanoseconds` is zero then this call will not wait. + * + * @param buffer The address of the first sample. + * @param numFrames Number of frames to read. Only complete frames will be read. + * @param timeoutNanoseconds Maximum number of nanoseconds to wait for completion. + * @return a ResultWithValue which has a result of Result::OK and a value containing the number + * of frames actually read, or result of Result::Error*. + */ + virtual ResultWithValue read(void* /* buffer */, + int32_t /* numFrames */, + int64_t /* timeoutNanoseconds */) { + return ResultWithValue(Result::ErrorUnimplemented); + } + + /** + * Get the underlying audio API which the stream uses. + * + * @return the API that this stream uses. + */ + virtual AudioApi getAudioApi() const = 0; + + /** + * Returns true if the underlying audio API is AAudio. + * + * @return true if this stream is implemented using the AAudio API. + */ + bool usesAAudio() const { + return getAudioApi() == AudioApi::AAudio; + } + + /** + * Only for debugging. Do not use in production. + * If you need to call this method something is wrong. + * If you think you need it for production then please let us know + * so we can modify Oboe so that you don't need this. + * + * @return nullptr or a pointer to a stream from the system API + */ + virtual void *getUnderlyingStream() const { + return nullptr; + } + + /** + * Update mFramesWritten. + * For internal use only. + */ + virtual void updateFramesWritten() = 0; + + /** + * Update mFramesRead. + * For internal use only. + */ + virtual void updateFramesRead() = 0; + + /* + * Swap old callback for new callback. + * This not atomic. + * This should only be used internally. + * @param dataCallback + * @return previous dataCallback + */ + AudioStreamDataCallback *swapDataCallback(AudioStreamDataCallback *dataCallback) { + AudioStreamDataCallback *previousCallback = mDataCallback; + mDataCallback = dataCallback; + return previousCallback; + } + + /* + * Swap old partial data callback for new callback. + * This not atomic. + * This should only be used internally. + * @param dataCallback + * @return previous dataCallback + */ + AudioStreamPartialDataCallback *swapPartialDataCallback( + AudioStreamPartialDataCallback *partialDataCallback) { + AudioStreamPartialDataCallback *previousPartialCallback = mPartialDataCallback; + mPartialDataCallback = partialDataCallback; + return previousPartialCallback; + } + + /* + * Swap old callback for new callback. + * This not atomic. + * This should only be used internally. + * @param errorCallback + * @return previous errorCallback + */ + AudioStreamErrorCallback *swapErrorCallback(AudioStreamErrorCallback *errorCallback) { + AudioStreamErrorCallback *previousCallback = mErrorCallback; + mErrorCallback = errorCallback; + return previousCallback; + } + + /** + * @return number of frames of data currently in the buffer + */ + ResultWithValue getAvailableFrames(); + + /** + * Wait until the stream has a minimum amount of data available in its buffer. + * This can be used with an EXCLUSIVE MMAP input stream to avoid reading data too close to + * the DSP write position, which may cause glitches. + * + * Starting with Oboe 1.7.1, the numFrames will be clipped internally against the + * BufferCapacity minus BurstSize. This is to prevent trying to wait for more frames + * than could possibly be available. In this case, the return value may be less than numFrames. + * Note that there may still be glitching if numFrames is too high. + * + * @param numFrames requested minimum frames available + * @param timeoutNanoseconds + * @return number of frames available, ErrorTimeout + */ + ResultWithValue waitForAvailableFrames(int32_t numFrames, + int64_t timeoutNanoseconds); + + /** + * @return last result passed from an error callback + */ + virtual oboe::Result getLastErrorCallbackResult() const { + return mErrorCallbackResult; + } + + + int32_t getDelayBeforeCloseMillis() const { + return mDelayBeforeCloseMillis; + } + + /** + * Set the time to sleep before closing the internal stream. + * + * Sometimes a callback can occur shortly after a stream has been stopped and + * even after a close! If the stream has been closed then the callback + * might access memory that has been freed, which could cause a crash. + * This seems to be more likely in Android P or earlier. + * But it can also occur in later versions. By sleeping, we give time for + * the callback threads to finish. + * + * Note that this only has an effect when OboeGlobals::areWorkaroundsEnabled() is true. + * + * @param delayBeforeCloseMillis time to sleep before close. + */ + void setDelayBeforeCloseMillis(int32_t delayBeforeCloseMillis) { + mDelayBeforeCloseMillis = delayBeforeCloseMillis; + } + + /** + * Enable or disable a device specific CPU performance hint. + * Runtime benchmarks such as the callback duration may be used to + * speed up the CPU and improve real-time performance. + * + * Note that this feature is device specific and may not be implemented. + * Also the benefits may vary by device. + * + * The flag will be checked in the Oboe data callback. If it transitions from false to true + * then the PerformanceHint feature will be started. + * This only needs to be called once for each stream. + * + * You may want to enable this if you have a dynamically changing workload + * and you notice that you are getting under-runs and glitches when your workload increases. + * This might happen, for example, if you suddenly go from playing one note to + * ten notes on a synthesizer. + * + * Try the "CPU Load" test in OboeTester if you would like to experiment with this interactively. + * + * On some devices, this may be implemented using the "ADPF" library. + * + * @param enabled true if you would like a performance boost, default is false + */ + void setPerformanceHintEnabled(bool enabled) { + mPerformanceHintEnabled = enabled; + } + + /** + * This only tells you if the feature has been requested. + * It does not tell you if the PerformanceHint feature is implemented or active on the device. + * + * @return true if set using setPerformanceHintEnabled(). + */ + bool isPerformanceHintEnabled() { + return mPerformanceHintEnabled; + } + + /** + * Use this to give the performance manager more information about your workload. + * You can call this at the beginning of the callback when you figure + * out what your workload will be. + * + * Call this if (1) you have called setPerformanceHintEnabled(true), and + * (2) you have a varying workload, and + * (3) you hear glitches when your workload suddenly increases. + * + * This might happen when you go from a single note to a big chord on a synthesizer. + * + * The workload can be in your own units. If you are synthesizing music + * then the workload could be the number of active voices. + * If your app is a game then it could be the number of sound effects. + * The units are arbitrary. They just have to be proportional to + * the estimated computational load. For example, if some of your voices take 20% + * more computation than a basic voice then assign 6 units to the complex voice + * and 5 units to the basic voice. + * + * The performance hint code can use this as an advance warning that the callback duration + * will probably increase. Rather than wait for the long duration and possibly under-run, + * we can boost the CPU immediately before we start doing the calculations. + * + * @param appWorkload workload in application units, such as number of voices + * @return OK or an error such as ErrorInvalidState if the PerformanceHint was not enabled. + */ + virtual oboe::Result reportWorkload([[maybe_unused]] int32_t appWorkload) { + return oboe::Result::ErrorUnimplemented; + } + + /** + * Informs the framework of an upcoming increase in the workload of an audio callback + * bound to this session. The user can specify whether the increase is expected to be + * on the CPU, GPU, or both. + * + * Sending hints for both CPU and GPU counts as two separate hints for the purposes of the + * rate limiter. + * + * @param cpu Indicates if the workload increase is expected to affect the CPU. + * @param gpu Indicates if the workload increase is expected to affect the GPU. + * @param debugName A required string used to identify this specific hint during + * tracing. This debug string will only be held for the duration of the + * method, and can be safely discarded after. + * + * This was introduced in Android API Level 36 + * + * @return Result::OK on success. + * Result::ErrorInvalidState if the PerformanceHint was not enabled. + * Result::ErrorClosed if AdpfWrapper::open() was not called. + * Result::ErrorUnimplemented if the API is not supported. + * Result::ErrorInvalidHandle if no hints were requested. + * Result::ErrorInvalidRate if the hint was rate limited. + * Result::ErrorNoService if communication with the system service has failed. + * Result::ErrorUnavailable if the hint is not supported. + */ + virtual oboe::Result notifyWorkloadIncrease([[maybe_unused]] bool cpu, + [[maybe_unused]] bool gpu, + [[maybe_unused]] const char* debugName) { + return oboe::Result::ErrorUnimplemented; + } + + /** + * Informs the framework of an upcoming reset in the workload of an audio callback + * bound to this session, or the imminent start of a new workload. The user can specify + * whether the reset is expected to affect the CPU, GPU, or both. + * + * Sending hints for both CPU and GPU counts as two separate hints for the purposes of the + * this load tracking. + * + * @param cpu Indicates if the workload reset is expected to affect the CPU. + * @param gpu Indicates if the workload reset is expected to affect the GPU. + * @param debugName A required string used to identify this specific hint during + * tracing. This debug string will only be held for the duration of the + * method, and can be safely discarded after. + * + * This was introduced in Android API Level 36 + * + * @return Result::OK on success. + * Result::ErrorInvalidState if the PerformanceHint was not enabled. + * Result::ErrorClosed if AdpfWrapper::open() was not called. + * Result::ErrorUnimplemented if the API is not supported. + * Result::ErrorInvalidHandle if no hints were requested. + * Result::ErrorInvalidRate if the hint was rate limited. + * Result::ErrorNoService if communication with the system service has failed. + * Result::ErrorUnavailable if the hint is not supported. + */ + virtual oboe::Result notifyWorkloadReset([[maybe_unused]] bool cpu, + [[maybe_unused]] bool gpu, + [[maybe_unused]] const char* debugName) { + return oboe::Result::ErrorUnimplemented; + } + + /** + * Informs the framework of an upcoming one-off expensive frame for an audio callback + * bound to this session. This frame will be treated as not representative of the workload as a + * whole, and it will be discarded the purposes of load tracking. The user can specify + * whether the workload spike is expected to be on the CPU, GPU, or both. + * + * Sending hints for both CPU and GPU counts as two separate hints for the purposes of the + * rate limiter. + * + * This was introduced in Android API Level 36 + * + * @param cpu Indicates if the workload spike is expected to affect the CPU. + * @param gpu Indicates if the workload spike is expected to affect the GPU. + * @param debugName A required string used to identify this specific hint during + * tracing. This debug string will only be held for the duration of the + * method, and can be safely discarded after. + * + * @return Result::OK on success. + * Result::ErrorInvalidState if the PerformanceHint was not enabled. + * Result::ErrorClosed if AdpfWrapper::open() was not called. + * Result::ErrorUnimplemented if the API is not supported. + * Result::ErrorInvalidHandle if no hints were requested. + * Result::ErrorInvalidRate if the hint was rate limited. + * Result::ErrorNoService if communication with the system service has failed. + * Result::ErrorUnavailable if the hint is not supported. + */ + virtual oboe::Result notifyWorkloadSpike([[maybe_unused]] bool cpu, + [[maybe_unused]] bool gpu, + [[maybe_unused]] const char* debugName) { + return oboe::Result::ErrorUnimplemented; + } + + virtual oboe::Result setOffloadDelayPadding([[maybe_unused]] int32_t delayInFrames, + [[maybe_unused]] int32_t paddingInFrames) { + return Result::ErrorUnimplemented; + } + + virtual ResultWithValue getOffloadDelay() { + return ResultWithValue(Result::ErrorUnimplemented); + } + + virtual ResultWithValue getOffloadPadding() { + return ResultWithValue(Result::ErrorUnimplemented); + } + + virtual oboe::Result setOffloadEndOfStream() { + return Result::ErrorUnimplemented; + } + + /** + * Flush all data from given position. If this operation returns successfully, the following + * data will be written from the returned position. + * + * This method will only available when the performance mode is + * PerformanceMode::PowerSavingOffloaded. + * + * The requested position must not be negative or greater than the written frames. The current + * written position can be known by querying getFramesWritten(). + * + * When clients request to flush from a certain position, the audio system will return the + * actual flushed position based on the requested position, playback latency, etc. The written + * position will be updated as the actual flush position. All data after actual flush position + * flushed. The client can provide data from actual flush position at next write operation or + * data callback request. When the stream is flushed, the stream end will be reset. The client + * must not write any data before this function returns. Otherwise, the data will be corrupted. + * When the method returns successfully and the stream is active, the client must write data + * immediately if little audio data remains. Otherwise, the stream will underrun. + * + * This was introduced in Android API Level 37. + * + * @param accuracy the accuracy requirement when flushing. The value must be one of the valid + * FlushFromAccuracy value. + * @param position the start point in frames to flush the stream. + * @return a result which the error code indicates if the stream is successfully flush or not + * and value as the successfully flushed position or suggested flushed position. + * Result::OK if the stream is successfully flushed. The value is the actual flushed + * position. + * Result::ErrorUnimplemented if it is not supported by the device. The value is the + * requested position. + * Result::ErrorIllegalArgument if the stream is not an output offload stream or the + * accuracy is not one of valid FlushFromAccuracy values. The value is the requested + * position. + * Result::ErrorOutOfRange if the provided position is negative or is greater than the + * frames written or the stream cannot flush from the requested position and + * FlushFromAccuracy::Accurate is requested. The value is suggested flushed position. + * Result::ErrorDisconnected if the stream is disconnected. The value is the requested + * position. + * Result::ErrorClosed if the stream is closed. The value is the requested position. + */ + virtual ResultWithValue flushFromFrame( + FlushFromAccuracy accuracy, int64_t positionInFrames) { + return ResultWithValue(Result::ErrorUnimplemented); + } + + /** + * Set playback parameters for the given stream. + * + * This was introduced in Android API Level 37. + * + * @param parameters a pointer of PlaybackParameters where current playback parameters + * will be written to on success. + * @return Result::OK if the playback parameters are set successfully. + * Result::ErrorIllegalArgument if the given stream is not an output stream or + * the requested parameters are invalid. + * Result::ErrorUnimplemented if the device or the stream doesn't support setting + * playback parameters. + * Result::ErrorInvalidState if the stream is not initialized successfully. + */ + virtual oboe::Result setPlaybackParameters(const PlaybackParameters& parameters) { + return Result::ErrorUnimplemented; + } + + /** + * Get current playback parameters for the given stream. + * + * This was introduced in Android API Level 37. + * + * @return a ResultWithValue which has a result of Result::OK and a value containing the + * playback parameters, or a result of Result::Error*. + */ + virtual ResultWithValue getPlaybackParameters() { + return ResultWithValue(Result::ErrorUnimplemented); + } + +protected: + + /** + * This is used to detect more than one error callback from a stream. + * These were bugs in some versions of Android that caused multiple error callbacks. + * Internal bug b/63087953 + * + * Calling this sets an atomic true and returns the previous value. + * + * @return false on first call, true on subsequent calls + */ + bool wasErrorCallbackCalled() { + return mErrorCallbackCalled.exchange(true); + } + + /** + * Wait for a transition from one state to another. + * @return OK if the endingState was observed, or ErrorUnexpectedState + * if any state that was not the startingState or endingState was observed + * or ErrorTimeout. + */ + virtual Result waitForStateTransition(StreamState startingState, + StreamState endingState, + int64_t timeoutNanoseconds); + + /** + * Override this to provide a default for when the application did not specify a callback. + * + * @param audioData + * @param numFrames + * @return result + */ + virtual DataCallbackResult onDefaultCallback(void* /* audioData */, int /* numFrames */) { + return DataCallbackResult::Stop; + } + + /** + * Override this to provide your own behaviour for the audio callback + * + * @param audioData container array which audio frames will be written into or read from + * @param numFrames number of frames which were read/written + * @return the result of the callback: stop or continue + * + */ + virtual DataCallbackResult fireDataCallback(void *audioData, int numFrames); + + /** + * Override this to provide your own behaviour for the parital audio callback + * + * @param audioData container array which audio frames will be written into or read from + * @param numFrames number of frames which were read/written + * @return the actual processed frames + */ + virtual int32_t firePartialDataCallback(void* audioData, int numFrames); + + /** + * @return true if callbacks may be called + */ + bool isDataCallbackEnabled() { + return mDataCallbackEnabled; + } + + /** + * This can be set false internally to prevent callbacks + * after DataCallbackResult::Stop has been returned. + */ + void setDataCallbackEnabled(bool enabled) { + mDataCallbackEnabled = enabled; + } + + /** + * This should only be called as a stream is being opened. + * Otherwise we might override setDelayBeforeCloseMillis(). + */ + void calculateDefaultDelayBeforeCloseMillis(); + + /** + * Try to avoid a race condition when closing. + */ + void sleepBeforeClose() { + if (mDelayBeforeCloseMillis > 0) { + usleep(mDelayBeforeCloseMillis * 1000); + } + } + + /** + * This may be called internally at the beginning of a callback. + */ + virtual void beginPerformanceHintInCallback() {} + + /** + * This may be called internally at the end of a callback. + * @param numFrames passed to the callback + */ + virtual void endPerformanceHintInCallback(int32_t /*numFrames*/) {} + + /** + * This will be called when the stream is closed just in case performance hints were enabled. + */ + virtual void closePerformanceHint() {} + + /* + * Set a weak_ptr to this stream from the shared_ptr so that we can + * later use a shared_ptr in the error callback. + */ + void setWeakThis(std::shared_ptr &sharedStream) { + mWeakThis = sharedStream; + } + + /* + * Make a shared_ptr that will prevent this stream from being deleted. + */ + std::shared_ptr lockWeakThis() { + return mWeakThis.lock(); + } + + std::weak_ptr mWeakThis; // weak pointer to this object + + /** + * Number of frames which have been written into the stream + * + * This is signed integer to match the counters in AAudio. + * At audio rates, the counter will overflow in about six million years. + */ + std::atomic mFramesWritten{}; + + /** + * Number of frames which have been read from the stream. + * + * This is signed integer to match the counters in AAudio. + * At audio rates, the counter will overflow in about six million years. + */ + std::atomic mFramesRead{}; + + std::mutex mLock; // for synchronizing start/stop/close + + oboe::Result mErrorCallbackResult = oboe::Result::OK; + + /** + * Number of frames which will be copied to/from the audio device in a single read/write + * operation + */ + int32_t mFramesPerBurst = kUnspecified; + + // Time to sleep in order to prevent a race condition with a callback after a close(). + // Two milliseconds may be enough but 10 msec is even safer. + static constexpr int kMinDelayBeforeCloseMillis = 10; + static constexpr int kMaxDelayBeforeCloseMillis = 100; + int32_t mDelayBeforeCloseMillis = kMinDelayBeforeCloseMillis; + +private: + + // Log the scheduler if it changes. + void checkScheduler(); + int mPreviousScheduler = -1; + + std::atomic mDataCallbackEnabled{false}; + std::atomic mErrorCallbackCalled{false}; + + std::atomic mPerformanceHintEnabled{false}; // set only by app +}; + +/** + * This struct is a stateless functor which closes an AudioStream prior to its deletion. + * This means it can be used to safely delete a smart pointer referring to an open stream. + */ + struct StreamDeleterFunctor { + void operator()(AudioStream *audioStream) { + if (audioStream) { + audioStream->close(); + } + delete audioStream; + } + }; +} // namespace oboe + +#endif /* OBOE_STREAM_H_ */ diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_Definitions_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_Definitions_android.h new file mode 100644 index 0000000..ecc9d78 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_Definitions_android.h @@ -0,0 +1,1217 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_DEFINITIONS_H +#define OBOE_DEFINITIONS_H + +#include +#include + +// Oboe needs to be able to build on old NDKs so we use hard coded constants. +// The correctness of these constants is verified in "aaudio/AAudioLoader.cpp". + +namespace oboe { + + /** + * Represents any attribute, property or value which hasn't been specified. + */ + constexpr int32_t kUnspecified = 0; + + // TODO: Investigate using std::chrono + /** + * The number of nanoseconds in a microsecond. 1,000. + */ + constexpr int64_t kNanosPerMicrosecond = 1000; + + /** + * The number of nanoseconds in a millisecond. 1,000,000. + */ + constexpr int64_t kNanosPerMillisecond = kNanosPerMicrosecond * 1000; + + /** + * The number of milliseconds in a second. 1,000. + */ + constexpr int64_t kMillisPerSecond = 1000; + + /** + * The number of nanoseconds in a second. 1,000,000,000. + */ + constexpr int64_t kNanosPerSecond = kNanosPerMillisecond * kMillisPerSecond; + + /** + * The state of the audio stream. + */ + enum class StreamState : int32_t { // aaudio_stream_state_t + Uninitialized = 0, // AAUDIO_STREAM_STATE_UNINITIALIZED, + Unknown = 1, // AAUDIO_STREAM_STATE_UNKNOWN, + Open = 2, // AAUDIO_STREAM_STATE_OPEN, + Starting = 3, // AAUDIO_STREAM_STATE_STARTING, + Started = 4, // AAUDIO_STREAM_STATE_STARTED, + Pausing = 5, // AAUDIO_STREAM_STATE_PAUSING, + Paused = 6, // AAUDIO_STREAM_STATE_PAUSED, + Flushing = 7, // AAUDIO_STREAM_STATE_FLUSHING, + Flushed = 8, // AAUDIO_STREAM_STATE_FLUSHED, + Stopping = 9, // AAUDIO_STREAM_STATE_STOPPING, + Stopped = 10, // AAUDIO_STREAM_STATE_STOPPED, + Closing = 11, // AAUDIO_STREAM_STATE_CLOSING, + Closed = 12, // AAUDIO_STREAM_STATE_CLOSED, + Disconnected = 13, // AAUDIO_STREAM_STATE_DISCONNECTED, + }; + + /** + * The direction of the stream. + */ + enum class Direction : int32_t { // aaudio_direction_t + + /** + * Used for playback. + */ + Output = 0, // AAUDIO_DIRECTION_OUTPUT, + + /** + * Used for recording. + */ + Input = 1, // AAUDIO_DIRECTION_INPUT, + }; + + /** + * The format of audio samples. + */ + enum class AudioFormat : int32_t { // aaudio_format_t + /** + * Invalid format. + */ + Invalid = -1, // AAUDIO_FORMAT_INVALID, + + /** + * Unspecified format. Format will be decided by Oboe. + * When calling getHardwareFormat(), this will be returned if + * the API is not supported. + */ + Unspecified = 0, // AAUDIO_FORMAT_UNSPECIFIED, + + /** + * Signed 16-bit integers. + */ + I16 = 1, // AAUDIO_FORMAT_PCM_I16, + + /** + * Single precision floating point. + * + * This is the recommended format for most applications. + * But note that the use of Float may prevent the opening of + * a low-latency input path on OpenSL ES or Legacy AAudio streams. + */ + Float = 2, // AAUDIO_FORMAT_PCM_FLOAT, + + /** + * Signed 24-bit integers, packed into 3 bytes. + * + * Note that the use of this format does not guarantee that + * the full precision will be provided. The underlying device may + * be using I16 format. + * + * Added in API 31 (S). + */ + I24 = 3, // AAUDIO_FORMAT_PCM_I24_PACKED + + /** + * Signed 32-bit integers. + * + * Note that the use of this format does not guarantee that + * the full precision will be provided. The underlying device may + * be using I16 format. + * + * Added in API 31 (S). + */ + I32 = 4, // AAUDIO_FORMAT_PCM_I32 + + /** + * This format is used for compressed audio wrapped in IEC61937 for HDMI + * or S/PDIF passthrough. + * + * Unlike PCM playback, the Android framework is not able to do format + * conversion for IEC61937. In that case, when IEC61937 is requested, sampling + * rate and channel count or channel mask must be specified. Otherwise, it may + * fail when opening the stream. Apps are able to get the correct configuration + * for the playback by calling AudioManager#getDevices(int). + * + * Available since API 34 (U). + */ + IEC61937 = 5, // AAUDIO_FORMAT_IEC61937 + + /** + * This format is used for audio compressed in MP3 format. + */ + MP3 = 6, // AAUDIO_FORMAT_MP3 + + /** + * This format is used for audio compressed in AAC LC format. + */ + AAC_LC, // AAUDIO_FORMAT_AAC_LC + + /** + * This format is used for audio compressed in AAC HE V1 format. + */ + AAC_HE_V1, // AAUDIO_FORMAT_AAC_HE_V1, + + /** + * This format is used for audio compressed in AAC HE V2 format. + */ + AAC_HE_V2, // AAUDIO_FORMAT_AAC_HE_V2 + + /** + * This format is used for audio compressed in AAC ELD format. + */ + AAC_ELD, // AAUDIO_FORMAT_AAC_ELD + + /** + * This format is used for audio compressed in AAC XHE format. + */ + AAC_XHE, // AAUDIO_FORMAT_AAC_XHE + + /** + * This format is used for audio compressed in OPUS. + */ + OPUS, // AAUDIO_FORMAT_OPUS + }; + + /** + * The result of an audio callback. + */ + enum class DataCallbackResult : int32_t { // aaudio_data_callback_result_t + // Indicates to the caller that the callbacks should continue. + Continue = 0, // AAUDIO_CALLBACK_RESULT_CONTINUE, + + // Indicates to the caller that the callbacks should stop immediately. + Stop = 1, // AAUDIO_CALLBACK_RESULT_STOP, + }; + + /** + * The result of an operation. All except the `OK` result indicates that an error occurred. + * The `Result` can be converted into a human readable string using `convertToText`. + */ + enum class Result : int32_t { // aaudio_result_t + OK = 0, // AAUDIO_OK + ErrorBase = -900, // AAUDIO_ERROR_BASE, + ErrorDisconnected = -899, // AAUDIO_ERROR_DISCONNECTED, + ErrorIllegalArgument = -898, // AAUDIO_ERROR_ILLEGAL_ARGUMENT, + ErrorInternal = -896, // AAUDIO_ERROR_INTERNAL, + ErrorInvalidState = -895, // AAUDIO_ERROR_INVALID_STATE, + ErrorInvalidHandle = -892, // AAUDIO_ERROR_INVALID_HANDLE, + ErrorUnimplemented = -890, // AAUDIO_ERROR_UNIMPLEMENTED, + ErrorUnavailable = -889, // AAUDIO_ERROR_UNAVAILABLE, + ErrorNoFreeHandles = -888, // AAUDIO_ERROR_NO_FREE_HANDLES, + ErrorNoMemory = -887, // AAUDIO_ERROR_NO_MEMORY, + ErrorNull = -886, // AAUDIO_ERROR_NULL, + ErrorTimeout = -885, // AAUDIO_ERROR_TIMEOUT, + ErrorWouldBlock = -884, // AAUDIO_ERROR_WOULD_BLOCK, + ErrorInvalidFormat = -883, // AAUDIO_ERROR_INVALID_FORMAT, + ErrorOutOfRange = -882, // AAUDIO_ERROR_OUT_OF_RANGE, + ErrorNoService = -881, // AAUDIO_ERROR_NO_SERVICE, + ErrorInvalidRate = -880, // AAUDIO_ERROR_INVALID_RATE, + // Reserved for future AAudio result types + Reserved1, + Reserved2, + Reserved3, + Reserved4, + Reserved5, + Reserved6, + Reserved7, + Reserved8, + Reserved9, + Reserved10, + ErrorClosed = -869, + }; + + /** + * The sharing mode of the audio stream. + */ + enum class SharingMode : int32_t { // aaudio_sharing_mode_t + + /** + * This will be the only stream using a particular source or sink. + * This mode will provide the lowest possible latency. + * You should close EXCLUSIVE streams immediately when you are not using them. + * + * If you do not need the lowest possible latency then we recommend using Shared, + * which is the default. + */ + Exclusive = 0, // AAUDIO_SHARING_MODE_EXCLUSIVE, + + /** + * Multiple applications can share the same device. + * The data from output streams will be mixed by the audio service. + * The data for input streams will be distributed by the audio service. + * + * This will have higher latency than the EXCLUSIVE mode. + */ + Shared = 1, // AAUDIO_SHARING_MODE_SHARED, + }; + + /** + * The performance mode of the audio stream. + */ + enum class PerformanceMode : int32_t { // aaudio_performance_mode_t + + /** + * No particular performance needs. Default. + */ + None = 10, // AAUDIO_PERFORMANCE_MODE_NONE, + + /** + * Extending battery life is most important. + */ + PowerSaving = 11, // AAUDIO_PERFORMANCE_MODE_POWER_SAVING, + + /** + * Reducing latency is most important. + */ + LowLatency = 12, // AAUDIO_PERFORMANCE_MODE_LOW_LATENCY + + /** + * Extending battery life is more important than low latency. + * + * This mode is not supported in input streams. + * This mode will play through the offloaded audio path to save battery life. + * With the offload playback, the default data callback size will be large and it + * allows data feeding thread to sleep longer time after sending enough data. + */ + PowerSavingOffloaded = 13, // AAUDIO_PERFORMANCE_MODE_POWER_SAVING_OFFLOADED + }; + + /** + * The underlying audio API used by the audio stream. + */ + enum class AudioApi : int32_t { + /** + * Try to use AAudio. If not available then use OpenSL ES. + */ + Unspecified = kUnspecified, + + /** + * Use OpenSL ES. + * Note that OpenSL ES is deprecated in Android 13, API 30 and above. + */ + OpenSLES, + + /** + * Try to use AAudio. Fail if unavailable. + * AAudio was first supported in Android 8, API 26 and above. + * It is only recommended for API 27 and above. + */ + AAudio + }; + + /** + * Specifies the quality of the sample rate conversion performed by Oboe. + * Higher quality will require more CPU load. + * Higher quality conversion will probably be implemented using a sinc based resampler. + */ + enum class SampleRateConversionQuality : int32_t { + /** + * No conversion by Oboe. Underlying APIs may still do conversion. + */ + None, + /** + * Fastest conversion but may not sound great. + * This may be implemented using bilinear interpolation. + */ + Fastest, + /** + * Low quality conversion with 8 taps. + */ + Low, + /** + * Medium quality conversion with 16 taps. + */ + Medium, + /** + * High quality conversion with 32 taps. + */ + High, + /** + * Highest quality conversion, which may be expensive in terms of CPU. + */ + Best, + }; + + /** + * The Usage attribute expresses *why* you are playing a sound, what is this sound used for. + * This information is used by certain platforms or routing policies + * to make more refined volume or routing decisions. + * + * Note that these match the equivalent values in AudioAttributes in the Android Java API. + * + * This attribute only has an effect on Android API 28+. + */ + enum class Usage : int32_t { // aaudio_usage_t + /** + * Use this for streaming media, music performance, video, podcasts, etcetera. + */ + Media = 1, // AAUDIO_USAGE_MEDIA + + /** + * Use this for voice over IP, telephony, etcetera. + */ + VoiceCommunication = 2, // AAUDIO_USAGE_VOICE_COMMUNICATION + + /** + * Use this for sounds associated with telephony such as busy tones, DTMF, etcetera. + */ + VoiceCommunicationSignalling = 3, // AAUDIO_USAGE_VOICE_COMMUNICATION_SIGNALLING + + /** + * Use this to demand the users attention. + */ + Alarm = 4, // AAUDIO_USAGE_ALARM + + /** + * Use this for notifying the user when a message has arrived or some + * other background event has occured. + */ + Notification = 5, // AAUDIO_USAGE_NOTIFICATION + + /** + * Use this when the phone rings. + */ + NotificationRingtone = 6, // AAUDIO_USAGE_NOTIFICATION_RINGTONE + + /** + * Use this to attract the users attention when, for example, the battery is low. + */ + NotificationEvent = 10, // AAUDIO_USAGE_NOTIFICATION_EVENT + + /** + * Use this for screen readers, etcetera. + */ + AssistanceAccessibility = 11, // AAUDIO_USAGE_ASSISTANCE_ACCESSIBILITY + + /** + * Use this for driving or navigation directions. + */ + AssistanceNavigationGuidance = 12, // AAUDIO_USAGE_ASSISTANCE_NAVIGATION_GUIDANCE + + /** + * Use this for user interface sounds, beeps, etcetera. + */ + AssistanceSonification = 13, // AAUDIO_USAGE_ASSISTANCE_SONIFICATION + + /** + * Use this for game audio and sound effects. + */ + Game = 14, // AAUDIO_USAGE_GAME + + /** + * Use this for audio responses to user queries, audio instructions or help utterances. + */ + Assistant = 16, // AAUDIO_USAGE_ASSISTANT + }; + + + /** + * The ContentType attribute describes *what* you are playing. + * It expresses the general category of the content. This information is optional. + * But in case it is known (for instance {@link Movie} for a + * movie streaming service or {@link Speech} for + * an audio book application) this information might be used by the audio framework to + * enforce audio focus. + * + * Note that these match the equivalent values in AudioAttributes in the Android Java API. + * + * This attribute only has an effect on Android API 28+. + */ + enum ContentType : int32_t { // aaudio_content_type_t + + /** + * Use this for spoken voice, audio books, etcetera. + */ + Speech = 1, // AAUDIO_CONTENT_TYPE_SPEECH + + /** + * Use this for pre-recorded or live music. + */ + Music = 2, // AAUDIO_CONTENT_TYPE_MUSIC + + /** + * Use this for a movie or video soundtrack. + */ + Movie = 3, // AAUDIO_CONTENT_TYPE_MOVIE + + /** + * Use this for sound is designed to accompany a user action, + * such as a click or beep sound made when the user presses a button. + */ + Sonification = 4, // AAUDIO_CONTENT_TYPE_SONIFICATION + }; + + /** + * Defines the audio source. + * An audio source defines both a default physical source of audio signal, and a recording + * configuration. + * + * Note that these match the equivalent values in MediaRecorder.AudioSource in the Android Java API. + * + * This attribute only has an effect on Android API 28+. + */ + enum InputPreset : int32_t { // aaudio_input_preset_t + /** + * Use this preset when other presets do not apply. + */ + Generic = 1, // AAUDIO_INPUT_PRESET_GENERIC + + /** + * Use this preset when recording video. + */ + Camcorder = 5, // AAUDIO_INPUT_PRESET_CAMCORDER + + /** + * Use this preset when doing speech recognition. + */ + VoiceRecognition = 6, // AAUDIO_INPUT_PRESET_VOICE_RECOGNITION + + /** + * Use this preset when doing telephony or voice messaging. + */ + VoiceCommunication = 7, // AAUDIO_INPUT_PRESET_VOICE_COMMUNICATION + + /** + * Use this preset to obtain an input with no effects. + * Note that this input will not have automatic gain control + * so the recorded volume may be very low. + */ + Unprocessed = 9, // AAUDIO_INPUT_PRESET_UNPROCESSED + + /** + * Use this preset for capturing audio meant to be processed in real time + * and played back for live performance (e.g karaoke). + * The capture path will minimize latency and coupling with playback path. + */ + VoicePerformance = 10, // AAUDIO_INPUT_PRESET_VOICE_PERFORMANCE + + }; + + /** + * This attribute can be used to allocate a session ID to the audio stream. + * + * This attribute only has an effect on Android API 28+. + */ + enum SessionId { + /** + * Do not allocate a session ID. + * Effects cannot be used with this stream. + * Default. + */ + None = -1, // AAUDIO_SESSION_ID_NONE + + /** + * Allocate a session ID that can be used to attach and control + * effects using the Java AudioEffects API. + * Note that the use of this flag may result in higher latency. + * + * Note that this matches the value of AudioManager.AUDIO_SESSION_ID_GENERATE. + */ + Allocate = 0, // AAUDIO_SESSION_ID_ALLOCATE + }; + + /** + * The channel count of the audio stream. The underlying type is `int32_t`. + * Use of this enum is convenient to avoid "magic" + * numbers when specifying the channel count. + * + * For example, you can write + * `builder.setChannelCount(ChannelCount::Stereo)` + * rather than `builder.setChannelCount(2)` + * + */ + enum ChannelCount : int32_t { + /** + * Audio channel count definition, use Mono or Stereo + */ + Unspecified = kUnspecified, + + /** + * Use this for mono audio + */ + Mono = 1, + + /** + * Use this for stereo audio. + */ + Stereo = 2, + }; + + /** + * The channel mask of the audio stream. The underlying type is `uint32_t`. + * Use of this enum is convenient. + * + * ChannelMask::Unspecified means this is not specified. + * The rest of the enums are channel position masks. + * Use the combinations of the channel position masks defined below instead of + * using those values directly. + * + * Channel masks are for input only, output only, or both input and output. + * These channel masks are different than those defined in AudioFormat.java. + * If an app gets a channel mask from Java API and wants to use it in Oboe, + * conversion should be done by the app. + */ + enum class ChannelMask : uint32_t { // aaudio_channel_mask_t + Unspecified = kUnspecified, + FrontLeft = 1 << 0, + FrontRight = 1 << 1, + FrontCenter = 1 << 2, + LowFrequency = 1 << 3, + BackLeft = 1 << 4, + BackRight = 1 << 5, + FrontLeftOfCenter = 1 << 6, + FrontRightOfCenter = 1 << 7, + BackCenter = 1 << 8, + SideLeft = 1 << 9, + SideRight = 1 << 10, + TopCenter = 1 << 11, + TopFrontLeft = 1 << 12, + TopFrontCenter = 1 << 13, + TopFrontRight = 1 << 14, + TopBackLeft = 1 << 15, + TopBackCenter = 1 << 16, + TopBackRight = 1 << 17, + TopSideLeft = 1 << 18, + TopSideRight = 1 << 19, + BottomFrontLeft = 1 << 20, + BottomFrontCenter = 1 << 21, + BottomFrontRight = 1 << 22, + LowFrequency2 = 1 << 23, + FrontWideLeft = 1 << 24, + FrontWideRight = 1 << 25, + + /** + * Supported for Input and Output + */ + Mono = FrontLeft, + + /** + * Supported for Input and Output + */ + Stereo = FrontLeft | + FrontRight, + + /** + * Supported for only Output + */ + CM2Point1 = FrontLeft | + FrontRight | + LowFrequency, + + /** + * Supported for only Output + */ + Tri = FrontLeft | + FrontRight | + FrontCenter, + + /** + * Supported for only Output + */ + TriBack = FrontLeft | + FrontRight | + BackCenter, + + /** + * Supported for only Output + */ + CM3Point1 = FrontLeft | + FrontRight | + FrontCenter | + LowFrequency, + + /** + * Supported for Input and Output + */ + CM2Point0Point2 = FrontLeft | + FrontRight | + TopSideLeft | + TopSideRight, + + /** + * Supported for Input and Output + */ + CM2Point1Point2 = CM2Point0Point2 | + LowFrequency, + + /** + * Supported for Input and Output + */ + CM3Point0Point2 = FrontLeft | + FrontRight | + FrontCenter | + TopSideLeft | + TopSideRight, + + /** + * Supported for Input and Output + */ + CM3Point1Point2 = CM3Point0Point2 | + LowFrequency, + + /** + * Supported for only Output + */ + Quad = FrontLeft | + FrontRight | + BackLeft | + BackRight, + + /** + * Supported for only Output + */ + QuadSide = FrontLeft | + FrontRight | + SideLeft | + SideRight, + + /** + * Supported for only Output + */ + Surround = FrontLeft | + FrontRight | + FrontCenter | + BackCenter, + + /** + * Supported for only Output + */ + Penta = Quad | + FrontCenter, + + /** + * Supported for Input and Output. aka 5Point1Back + */ + CM5Point1 = FrontLeft | + FrontRight | + FrontCenter | + LowFrequency | + BackLeft | + BackRight, + + /** + * Supported for only Output + */ + CM5Point1Side = FrontLeft | + FrontRight | + FrontCenter | + LowFrequency | + SideLeft | + SideRight, + + /** + * Supported for only Output + */ + CM6Point1 = FrontLeft | + FrontRight | + FrontCenter | + LowFrequency | + BackLeft | + BackRight | + BackCenter, + + /** + * Supported for only Output + */ + CM7Point1 = CM5Point1 | + SideLeft | + SideRight, + + /** + * Supported for only Output + */ + CM5Point1Point2 = CM5Point1 | + TopSideLeft | + TopSideRight, + + /** + * Supported for only Output + */ + CM5Point1Point4 = CM5Point1 | + TopFrontLeft | + TopFrontRight | + TopBackLeft | + TopBackRight, + + /** + * Supported for only Output + */ + CM7Point1Point2 = CM7Point1 | + TopSideLeft | + TopSideRight, + + /** + * Supported for only Output + */ + CM7Point1Point4 = CM7Point1 | + TopFrontLeft | + TopFrontRight | + TopBackLeft | + TopBackRight, + + /** + * Supported for only Output + */ + CM9Point1Point4 = CM7Point1Point4 | + FrontWideLeft | + FrontWideRight, + + /** + * Supported for only Output + */ + CM9Point1Point6 = CM9Point1Point4 | + TopSideLeft | + TopSideRight, + + /** + * Supported for only Input + */ + FrontBack = FrontCenter | + BackCenter, + }; + + /** + * The spatialization behavior of the audio stream. + */ + enum class SpatializationBehavior : int32_t { + + /** + * Constant indicating that the spatialization behavior is not specified. + */ + Unspecified = kUnspecified, + + /** + * Constant indicating the audio content associated with these attributes will follow the + * default platform behavior with regards to which content will be spatialized or not. + */ + Auto = 1, + + /** + * Constant indicating the audio content associated with these attributes should never + * be spatialized. + */ + Never = 2, + }; + + /** + * The PrivacySensitiveMode attribute determines whether an input stream can be shared + * with another privileged app, for example the Assistant. + * + * This allows to override the default behavior tied to the audio source (e.g + * InputPreset::VoiceCommunication is private by default but InputPreset::Unprocessed is not). + */ + enum class PrivacySensitiveMode : int32_t { + + /** + * When not explicitly requested, set privacy sensitive mode according to input preset: + * communication and camcorder captures are considered privacy sensitive by default. + */ + Unspecified = kUnspecified, + + /** + * Privacy sensitive mode disabled. + */ + Disabled = 1, + + /** + * Privacy sensitive mode enabled. + */ + Enabled = 2, + }; + + /** + * Specifies whether audio may or may not be captured by other apps or the system for an + * output stream. + * + * Note that these match the equivalent values in AudioAttributes in the Android Java API. + * + * Added in API level 29 for AAudio. + */ + enum class AllowedCapturePolicy : int32_t { + /** + * When not explicitly requested, set privacy sensitive mode according to the Usage. + * This should behave similarly to setting AllowedCapturePolicy::All. + */ + Unspecified = kUnspecified, + /** + * Indicates that the audio may be captured by any app. + * + * For privacy, the following Usages can not be recorded: VoiceCommunication*, + * Notification*, Assistance* and Assistant. + * + * On Android Q, only Usage::Game and Usage::Media may be captured. + * + * See ALLOW_CAPTURE_BY_ALL in the AudioAttributes Java API. + */ + All = 1, + /** + * Indicates that the audio may only be captured by system apps. + * + * System apps can capture for many purposes like accessibility, user guidance... + * but have strong restriction. See ALLOW_CAPTURE_BY_SYSTEM in the AudioAttributes Java API + * for what the system apps can do with the capture audio. + */ + System = 2, + /** + * Indicates that the audio may not be recorded by any app, even if it is a system app. + * + * It is encouraged to use AllowedCapturePolicy::System instead of this value as system apps + * provide significant and useful features for the user (eg. accessibility). + * See ALLOW_CAPTURE_BY_NONE in the AudioAttributes Java API + */ + None = 3, + }; + + /** + * Audio device type. + * + * Note that these match the device types defined in android/media/AudioDeviceInfo.java + * and the definitions of AAudio_DeviceType in AAudio.h. + * + * Added in API level 36 for AAudio. + */ + enum class DeviceType : int32_t { + /** + * A device type describing the attached earphone speaker. + */ + BuiltinEarpiece = 1, + + /** + * A device type describing the speaker system (i.e. a mono speaker or stereo speakers) + * built in a device. + */ + BuiltinSpeaker = 2, + + /** + * A device type describing a headset, which is the combination of a headphones and + * microphone. + */ + WiredHeadset = 3, + + /** + * A device type describing a pair of wired headphones. + */ + WiredHeadphones = 4, + + /** + * A device type describing an analog line-level connection. + */ + LineAnalog = 5, + + /** + * A device type describing a digital line connection (e.g. SPDIF). + */ + LineDigital = 6, + + /** + * A device type describing a Bluetooth device typically used for telephony. + */ + BluetoothSco = 7, + + /** + * A device type describing a Bluetooth device supporting the A2DP profile. + */ + BluetoothA2dp = 8, + + /** + * A device type describing an HDMI connection . + */ + Hdmi = 9, + + /** + * A device type describing the Audio Return Channel of an HDMI connection. + */ + HdmiArc = 10, + + /** + * A device type describing a USB audio device. + */ + UsbDevice = 11, + + /** + * A device type describing a USB audio device in accessory mode. + */ + UsbAccessory = 12, + + /** + * A device type describing the audio device associated with a dock. + */ + Dock = 13, + + /** + * A device type associated with the transmission of audio signals over FM. + */ + FM = 14, + + /** + * A device type describing the microphone(s) built in a device. + */ + BuiltinMic = 15, + + /** + * A device type for accessing the audio content transmitted over FM. + */ + FMTuner = 16, + + /** + * A device type for accessing the audio content transmitted over the TV tuner system. + */ + TVTuner = 17, + + /** + * A device type describing the transmission of audio signals over the telephony network. + */ + Telephony = 18, + + /** + * A device type describing the auxiliary line-level connectors. + */ + AuxLine = 19, + + /** + * A device type connected over IP. + */ + IP = 20, + + /** + * A type-agnostic device used for communication with external audio systems. + */ + Bus = 21, + + /** + * A device type describing a USB audio headset. + */ + UsbHeadset = 22, + + /** + * A device type describing a Hearing Aid. + */ + HearingAid = 23, + + /** + * A device type describing the speaker system (i.e. a mono speaker or stereo speakers) + * built in a device, that is specifically tuned for outputting sounds like notifications + * and alarms (i.e. sounds the user couldn't necessarily anticipate). + *

Note that this physical audio device may be the same as {@link #TYPE_BUILTIN_SPEAKER} + * but is driven differently to safely accommodate the different use case.

+ */ + BuiltinSpeakerSafe = 24, + + /** + * A device type for rerouting audio within the Android framework between mixes and + * system applications. + */ + RemoteSubmix = 25, + /** + * A device type describing a Bluetooth Low Energy (BLE) audio headset or headphones. + * Headphones are grouped with headsets when the device is a sink: + * the features of headsets and headphones with regard to playback are the same. + */ + BleHeadset = 26, + + /** + * A device type describing a Bluetooth Low Energy (BLE) audio speaker. + */ + BleSpeaker = 27, + + /** + * A device type describing the Enhanced Audio Return Channel of an HDMI connection. + */ + HdmiEarc = 29, + + /** + * A device type describing a Bluetooth Low Energy (BLE) broadcast group. + */ + BleBroadcast = 30, + + /** + * A device type describing the audio device associated with a dock using an + * analog connection. + */ + DockAnalog = 31 + }; + + /** + * MMAP policy is defined to describe how aaudio MMAP will be used. + * + * Added in API level 36. + */ + enum class MMapPolicy : int32_t { + /** + * When MMAP policy is not specified or the querying API is not supported. + */ + Unspecified = kUnspecified, + + /** + * AAudio MMAP is disabled and never used. + */ + Never = 1, + + /** + * AAudio MMAP support depends on device's availability. It will be used + * when it is possible or fallback to the normal path, where the audio data + * will be delivered via audio framework data pipeline. + */ + Auto, + + /** + * AAudio MMAP must be used or fail. + */ + Always + }; + + /** + * The values are defined to be used for the accuracy requirement when calling + * AudioStream.flushFromFrame. + */ + enum class FlushFromAccuracy : int32_t { + /** + * There is not requirement for frame accuracy when flushing, it is up to the OS + * to select a right position to flush from. + */ + Undefined = 0, // AAUDIO_FLUSH_FROM_ACCURACY_UNDEFINED + + /** + * The stream must be flushed from the requested position. If it is not possible to flush + * from the requested position, the stream must not be flushed. + */ + Accurate = 1, // AAUDIO_FLUSH_FROM_ACCURACY_ACCURATE + }; + + /** + * Behavior when the values for speed and / or pitch are out of the applicable range. + */ + enum class FallbackMode : int32_t { + /** + * It is up to the system to choose best handling. + */ + Default = 0, // AAUDIO_FALLBACK_MODE_DEFAULT + /** + * Play silence for parameter values that are out of range. + */ + Mute = 1, // AAUDIO_FALLBACK_MODE_MUTE + /** + * When the requested speed and or pitch is out of range, processing will be + * stopped and an error will be returned. + */ + Fail = 2, // AAUDIO_FALLBACK_MODE_FAIL + }; + + /** + * Algorithms used for time-stretching (preserving pitch while playing audio + * content at different speed). + */ + enum class StretchMode : int32_t { + /** + * Time-stretching algorithm is selected by the system. + */ + Default = 0, // AAUDIO_STRETCH_MODE_DEFAULT + /** + * Selects time-stretch algorithm best suitable for voice (speech) content. + */ + Voice = 1, // AAUDIO_STRETCH_MODE_VOICE + }; + + /** + * Structure for common playback params. + */ + struct PlaybackParameters { + /** + * See `FallbackMode`. + */ + FallbackMode fallbackMode; + /** + * See `StretchMode`. + */ + StretchMode stretchMode; + /** + * Increases or decreases the tonal frequency of the audio content. + * It is expressed as a multiplicative factor, where normal pitch is 1.0f. + * The pitch must be in range of [0.25f, 4.0f]. + */ + float pitch; + /** + * Increases or decreases the time to play back a set of audio frames. + * Normal speed is 1.0f. The speed must in range of [0.01f, 20.0f]. + */ + float speed; + }; + + /** + * On API 16 to 26 OpenSL ES will be used. When using OpenSL ES the optimal values for sampleRate and + * framesPerBurst are not known by the native code. + * On API 17+ these values should be obtained from the AudioManager using this code: + * + *

+     * // Note that this technique only works for built-in speakers and headphones.
+     * AudioManager myAudioMgr = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
+     * String sampleRateStr = myAudioMgr.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE);
+     * int defaultSampleRate = Integer.parseInt(sampleRateStr);
+     * String framesPerBurstStr = myAudioMgr.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER);
+     * int defaultFramesPerBurst = Integer.parseInt(framesPerBurstStr);
+     * 
+ * + * It can then be passed down to Oboe through JNI. + * + * AAudio will get the optimal framesPerBurst from the HAL and will ignore this value. + */ + class DefaultStreamValues { + + public: + + /** The default sample rate to use when opening new audio streams */ + static int32_t SampleRate; + /** The default frames per burst to use when opening new audio streams */ + static int32_t FramesPerBurst; + /** The default channel count to use when opening new audio streams */ + static int32_t ChannelCount; + + }; + + /** + * The time at which the frame at `position` was presented + */ + struct FrameTimestamp { + int64_t position; // in frames + int64_t timestamp; // in nanoseconds + }; + + class OboeGlobals { + public: + + static bool areWorkaroundsEnabled() { + return mWorkaroundsEnabled; + } + + /** + * Disable this when writing tests to reproduce bugs in AAudio or OpenSL ES + * that have workarounds in Oboe. + * @param enabled + */ + static void setWorkaroundsEnabled(bool enabled) { + mWorkaroundsEnabled = enabled; + } + + private: + static bool mWorkaroundsEnabled; + }; +} // namespace oboe + +#endif // OBOE_DEFINITIONS_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_FifoBuffer_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_FifoBuffer_android.h new file mode 100644 index 0000000..e7c389e --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_FifoBuffer_android.h @@ -0,0 +1,164 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_FIFOPROCESSOR_H +#define OBOE_FIFOPROCESSOR_H + +#include +#include + +#include "oboe_oboe_Definitions_android.h" + +#include "oboe_oboe_FifoControllerBase_android.h" + +namespace oboe { + +class FifoBuffer { +public: + /** + * Construct a `FifoBuffer`. + * + * @param bytesPerFrame amount of bytes for one frame + * @param capacityInFrames the capacity of frames in fifo + */ + FifoBuffer(uint32_t bytesPerFrame, uint32_t capacityInFrames); + + /** + * Construct a `FifoBuffer`. + * To be used if the storage allocation is done outside of FifoBuffer. + * + * @param bytesPerFrame amount of bytes for one frame + * @param capacityInFrames capacity of frames in fifo + * @param readCounterAddress address of read counter + * @param writeCounterAddress address of write counter + * @param dataStorageAddress address of storage + */ + FifoBuffer(uint32_t bytesPerFrame, + uint32_t capacityInFrames, + std::atomic *readCounterAddress, + std::atomic *writeCounterAddress, + uint8_t *dataStorageAddress); + + ~FifoBuffer(); + + /** + * Convert a number of frames in bytes. + * + * @return number of bytes + */ + int32_t convertFramesToBytes(int32_t frames); + + /** + * Read framesToRead or, if not enough, then read as many as are available. + * + * @param destination + * @param framesToRead number of frames requested + * @return number of frames actually read + */ + int32_t read(void *destination, int32_t framesToRead); + + /** + * Write framesToWrite or, if too enough, then write as many as the fifo are not empty. + * + * @param destination + * @param framesToWrite number of frames requested + * @return number of frames actually write + */ + int32_t write(const void *source, int32_t framesToWrite); + + /** + * Get the buffer capacity in frames. + * + * @return number of frames + */ + uint32_t getBufferCapacityInFrames() const; + + /** + * Calls read(). If all of the frames cannot be read then the remainder of the buffer + * is set to zero. + * + * @param destination + * @param framesToRead number of frames requested + * @return number of frames actually read + */ + int32_t readNow(void *destination, int32_t numFrames); + + /** + * Get the number of frames in the fifo. + * + * @return number of frames actually in the buffer + */ + uint32_t getFullFramesAvailable() { + return mFifo->getFullFramesAvailable(); + } + + /** + * Get the amount of bytes per frame. + * + * @return number of bytes per frame + */ + uint32_t getBytesPerFrame() const { + return mBytesPerFrame; + } + + /** + * Get the position of read counter. + * + * @return position of read counter + */ + uint64_t getReadCounter() const { + return mFifo->getReadCounter(); + } + + /** + * Set the position of read counter. + * + * @param n position of read counter + */ + void setReadCounter(uint64_t n) { + mFifo->setReadCounter(n); + } + + /** + * Get the position of write counter. + * + * @return position of write counter + */ + uint64_t getWriteCounter() { + return mFifo->getWriteCounter(); + } + + /** + * Set the position of write counter. + * + * @param n position of write counter + */ + void setWriteCounter(uint64_t n) { + mFifo->setWriteCounter(n); + } + +private: + uint32_t mBytesPerFrame; + uint8_t* mStorage; + bool mStorageOwned; // did this object allocate the storage? + std::unique_ptr mFifo; + uint64_t mFramesReadCount; + uint64_t mFramesUnderrunCount; +}; + +} // namespace oboe + +#endif //OBOE_FIFOPROCESSOR_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_FifoControllerBase_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_FifoControllerBase_android.h new file mode 100644 index 0000000..6c12b75 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_FifoControllerBase_android.h @@ -0,0 +1,112 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef NATIVEOBOE_FIFOCONTROLLERBASE_H +#define NATIVEOBOE_FIFOCONTROLLERBASE_H + +#include + +namespace oboe { + +/** + * Manage the read/write indices of a circular buffer. + * + * The caller is responsible for reading and writing the actual data. + * Note that the span of available frames may not be contiguous. They + * may wrap around from the end to the beginning of the buffer. In that + * case the data must be read or written in at least two blocks of frames. + * + */ + +class FifoControllerBase { + +public: + /** + * Construct a `FifoControllerBase`. + * + * @param totalFrames capacity of the circular buffer in frames + */ + FifoControllerBase(uint32_t totalFrames); + + virtual ~FifoControllerBase() = default; + + /** + * The frames available to read will be calculated from the read and write counters. + * The result will be clipped to the capacity of the buffer. + * If the buffer has underflowed then this will return zero. + * + * @return number of valid frames available to read. + */ + uint32_t getFullFramesAvailable() const; + + /** + * The index in a circular buffer of the next frame to read. + * + * @return read index position + */ + uint32_t getReadIndex() const; + + /** + * Advance read index from a number of frames. + * Equivalent of incrementReadCounter(numFrames). + * + * @param numFrames number of frames to advance the read index + */ + void advanceReadIndex(uint32_t numFrames); + + /** + * Get the number of frame that are not written yet. + * + * @return maximum number of frames that can be written without exceeding the threshold + */ + uint32_t getEmptyFramesAvailable() const; + + /** + * The index in a circular buffer of the next frame to write. + * + * @return index of the next frame to write + */ + uint32_t getWriteIndex() const; + + /** + * Advance write index from a number of frames. + * Equivalent of incrementWriteCounter(numFrames). + * + * @param numFrames number of frames to advance the write index + */ + void advanceWriteIndex(uint32_t numFrames); + + /** + * Get the frame capacity of the fifo. + * + * @return frame capacity + */ + uint32_t getFrameCapacity() const { return mTotalFrames; } + + virtual uint64_t getReadCounter() const = 0; + virtual void setReadCounter(uint64_t n) = 0; + virtual void incrementReadCounter(uint64_t n) = 0; + virtual uint64_t getWriteCounter() const = 0; + virtual void setWriteCounter(uint64_t n) = 0; + virtual void incrementWriteCounter(uint64_t n) = 0; + +private: + uint32_t mTotalFrames; +}; + +} // namespace oboe + +#endif //NATIVEOBOE_FIFOCONTROLLERBASE_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_FullDuplexStream_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_FullDuplexStream_android.h new file mode 100644 index 0000000..0430a11 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_FullDuplexStream_android.h @@ -0,0 +1,356 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_FULL_DUPLEX_STREAM_ +#define OBOE_FULL_DUPLEX_STREAM_ + +#include +#include "oboe_oboe_Definitions_android.h" +#include "oboe_oboe_AudioStream_android.h" +#include "oboe_oboe_AudioStreamCallback_android.h" + +namespace oboe { + +/** + * FullDuplexStream can be used to synchronize an input and output stream. + * + * For the builder of the output stream, call setDataCallback() with this object. + * + * When both streams are ready, onAudioReady() of the output stream will call onBothStreamsReady(). + * Callers must override onBothStreamsReady(). + * + * To ensure best results, open an output stream before the input stream. + * Call inputBuilder.setBufferCapacityInFrames(mOutputStream->getBufferCapacityInFrames() * 2). + * Also, call inputBuilder.setSampleRate(mOutputStream->getSampleRate()). + * + * Callers must call setInputStream() and setOutputStream(). + * Call start() to start both streams and stop() to stop both streams. + * Caller is responsible for closing both streams. + * + * Callers should handle error callbacks with setErrorCallback() for the output stream. + * When an error callback occurs for the output stream, Oboe will stop and close the output stream. + * The caller is responsible for stopping and closing the input stream. + * The caller should also reopen and restart both streams when the error callback is ErrorDisconnected. + * See the LiveEffect sample as an example of this. + * + */ +class FullDuplexStream : public AudioStreamDataCallback { +public: + FullDuplexStream() {} + virtual ~FullDuplexStream() = default; + + /** + * Sets the input stream. + * + * @deprecated Call setSharedInputStream(std::shared_ptr &stream) instead. + * @param stream the output stream + */ + void setInputStream(AudioStream *stream) { + mRawInputStream = stream; + } + + /** + * Sets the input stream. Calling this is mandatory. + * + * @param stream the output stream + */ + void setSharedInputStream(std::shared_ptr &stream) { + mSharedInputStream = stream; + } + + /** + * Gets the current input stream. This function tries to return the shared input stream if it + * is set before the raw input stream. + * + * @return pointer to an output stream or nullptr. + */ + AudioStream *getInputStream() { + if (mSharedInputStream) { + return mSharedInputStream.get(); + } else { + return mRawInputStream; + } + } + + /** + * Sets the output stream. + * + * @deprecated Call setSharedOutputStream(std::shared_ptr &stream) instead. + * @param stream the output stream + */ + void setOutputStream(AudioStream *stream) { + mRawOutputStream = stream; + } + + /** + * Sets the output stream. Calling this is mandatory. + * + * @param stream the output stream + */ + void setSharedOutputStream(std::shared_ptr &stream) { + mSharedOutputStream = stream; + } + + /** + * Gets the current output stream. This function tries to return the shared output stream if it + * is set before the raw output stream. + * + * @return pointer to an output stream or nullptr. + */ + AudioStream *getOutputStream() { + if (mSharedOutputStream) { + return mSharedOutputStream.get(); + } else { + return mRawOutputStream; + } + } + + /** + * Attempts to start both streams. Please call setInputStream() and setOutputStream() before + * calling this function. + * + * @return result of the operation + */ + virtual Result start() { + mCountCallbacksToDrain = kNumCallbacksToDrain; + mCountInputBurstsCushion = mNumInputBurstsCushion; + mCountCallbacksToDiscard = kNumCallbacksToDiscard; + + // Determine maximum size that could possibly be called. + int32_t bufferSize = getOutputStream()->getBufferCapacityInFrames() + * getOutputStream()->getChannelCount(); + if (bufferSize > mBufferSize) { + mInputBuffer = std::make_unique(bufferSize); + mBufferSize = bufferSize; + } + + oboe::Result result = getInputStream()->requestStart(); + if (result != oboe::Result::OK) { + return result; + } + return getOutputStream()->requestStart(); + } + + /** + * Stops both streams. Returns Result::OK if neither stream had an error during close. + * + * @return result of the operation + */ + virtual Result stop() { + Result outputResult = Result::OK; + Result inputResult = Result::OK; + if (getOutputStream()) { + outputResult = getOutputStream()->requestStop(); + } + if (getInputStream()) { + inputResult = getInputStream()->requestStop(); + } + if (outputResult != Result::OK) { + return outputResult; + } else { + return inputResult; + } + } + + /** + * Reads input from the input stream. Callers should not call this directly as this is called + * in onAudioReady(). + * + * @param numFrames + * @return result of the operation + */ + virtual ResultWithValue readInput(int32_t numFrames) { + return getInputStream()->read(mInputBuffer.get(), numFrames, 0 /* timeout */); + } + + /** + * Called when data is available on both streams. + * Caller should override this method. + * numInputFrames and numOutputFrames may be zero. + * + * @param inputData buffer containing input data + * @param numInputFrames number of input frames + * @param outputData a place to put output data + * @param numOutputFrames number of output frames + * @return DataCallbackResult::Continue or DataCallbackResult::Stop + */ + virtual DataCallbackResult onBothStreamsReady( + const void *inputData, + int numInputFrames, + void *outputData, + int numOutputFrames + ) = 0; + + /** + * Called when the output stream is ready to process audio. + * This in return calls onBothStreamsReady() when data is available on both streams. + * Callers should call this function when the output stream is ready. + * Callers must override onBothStreamsReady(). + * + * @param audioStream pointer to the associated stream + * @param audioData a place to put output data + * @param numFrames number of frames to be processed + * @return DataCallbackResult::Continue or DataCallbackResult::Stop + * + */ + DataCallbackResult onAudioReady( + AudioStream * /*audioStream*/, + void *audioData, + int numFrames) { + DataCallbackResult callbackResult = DataCallbackResult::Continue; + int32_t actualFramesRead = 0; + + // Silence the output. + int32_t numBytes = numFrames * getOutputStream()->getBytesPerFrame(); + memset(audioData, 0 /* value */, numBytes); + + if (mCountCallbacksToDrain > 0) { + // Drain the input. + int32_t totalFramesRead = 0; + do { + ResultWithValue result = readInput(numFrames); + if (!result) { + // Ignore errors because input stream may not be started yet. + break; + } + actualFramesRead = result.value(); + totalFramesRead += actualFramesRead; + } while (actualFramesRead > 0); + // Only counts if we actually got some data. + if (totalFramesRead > 0) { + mCountCallbacksToDrain--; + } + + } else if (mCountInputBurstsCushion > 0) { + // Let the input fill up a bit so we are not so close to the write pointer. + mCountInputBurstsCushion--; + + } else if (mCountCallbacksToDiscard > 0) { + mCountCallbacksToDiscard--; + // Ignore. Allow the input to reach to equilibrium with the output. + ResultWithValue resultAvailable = getInputStream()->getAvailableFrames(); + if (!resultAvailable) { + callbackResult = DataCallbackResult::Stop; + } else { + int32_t framesAvailable = resultAvailable.value(); + if (framesAvailable >= mMinimumFramesBeforeRead) { + ResultWithValue resultRead = readInput(numFrames); + if (!resultRead) { + callbackResult = DataCallbackResult::Stop; + } + } + } + } else { + int32_t framesRead = 0; + ResultWithValue resultAvailable = getInputStream()->getAvailableFrames(); + if (!resultAvailable) { + callbackResult = DataCallbackResult::Stop; + } else { + int32_t framesAvailable = resultAvailable.value(); + if (framesAvailable >= mMinimumFramesBeforeRead) { + // Read data into input buffer. + ResultWithValue resultRead = readInput(numFrames); + if (!resultRead) { + callbackResult = DataCallbackResult::Stop; + } else { + framesRead = resultRead.value(); + } + } + } + + if (callbackResult == DataCallbackResult::Continue) { + callbackResult = onBothStreamsReady(mInputBuffer.get(), framesRead, + audioData, numFrames); + } + } + + if (callbackResult == DataCallbackResult::Stop) { + getInputStream()->requestStop(); + } + + return callbackResult; + } + + /** + * + * This is a cushion between the DSP and the application processor cursors to prevent collisions. + * Typically 0 for latency measurements or 1 for glitch tests. + * + * @param numBursts number of bursts to leave in the input buffer as a cushion + */ + void setNumInputBurstsCushion(int32_t numBursts) { + mNumInputBurstsCushion = numBursts; + } + + /** + * Get the number of bursts left in the input buffer as a cushion. + * + * @return number of bursts in the input buffer as a cushion + */ + int32_t getNumInputBurstsCushion() const { + return mNumInputBurstsCushion; + } + + /** + * Minimum number of frames in the input stream buffer before calling readInput(). + * + * @param numFrames number of bursts in the input buffer as a cushion + */ + void setMinimumFramesBeforeRead(int32_t numFrames) { + mMinimumFramesBeforeRead = numFrames; + } + + /** + * Gets the minimum number of frames in the input stream buffer before calling readInput(). + * + * @return minimum number of frames before reading + */ + int32_t getMinimumFramesBeforeRead() const { + return mMinimumFramesBeforeRead; + } + +private: + + // TODO add getters and setters + static constexpr int32_t kNumCallbacksToDrain = 20; + static constexpr int32_t kNumCallbacksToDiscard = 30; + + // let input fill back up, usually 0 or 1 + int32_t mNumInputBurstsCushion = 0; + int32_t mMinimumFramesBeforeRead = 0; + + // We want to reach a state where the input buffer is empty and + // the output buffer is full. + // These are used in order. + // Drain several callback so that input is empty. + int32_t mCountCallbacksToDrain = kNumCallbacksToDrain; + // Let the input fill back up slightly so we don't run dry. + int32_t mCountInputBurstsCushion = mNumInputBurstsCushion; + // Discard some callbacks so the input and output reach equilibrium. + int32_t mCountCallbacksToDiscard = kNumCallbacksToDiscard; + + AudioStream *mRawInputStream = nullptr; + AudioStream *mRawOutputStream = nullptr; + std::shared_ptr mSharedInputStream; + std::shared_ptr mSharedOutputStream; + + int32_t mBufferSize = 0; + std::unique_ptr mInputBuffer; +}; + +} // namespace oboe + +#endif //OBOE_FULL_DUPLEX_STREAM_ diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_LatencyTuner_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_LatencyTuner_android.h new file mode 100644 index 0000000..efff393 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_LatencyTuner_android.h @@ -0,0 +1,150 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_LATENCY_TUNER_ +#define OBOE_LATENCY_TUNER_ + +#include +#include +#include "oboe_oboe_Definitions_android.h" +#include "oboe_oboe_AudioStream_android.h" + +namespace oboe { + +/** + * LatencyTuner can be used to dynamically tune the latency of an output stream. + * It adjusts the stream's bufferSize by monitoring the number of underruns. + * + * This only affects the latency associated with the first level of buffering that is closest + * to the application. It does not affect low latency in the HAL, or touch latency in the UI. + * + * Call tune() right before returning from your data callback function if using callbacks. + * Call tune() right before calling write() if using blocking writes. + * + * If you want to see the ongoing results of this tuning process then call + * stream->getBufferSize() periodically. + * + */ +class LatencyTuner { +public: + + /** + * Construct a new LatencyTuner object which will act on the given audio stream + * + * @param stream the stream who's latency will be tuned + */ + explicit LatencyTuner(AudioStream &stream); + + /** + * Construct a new LatencyTuner object which will act on the given audio stream. + * + * @param stream the stream who's latency will be tuned + * @param the maximum buffer size which the tune() operation will set the buffer size to + */ + explicit LatencyTuner(AudioStream &stream, int32_t maximumBufferSize); + + /** + * Adjust the bufferSizeInFrames to optimize latency. + * It will start with a low latency and then raise it if an underrun occurs. + * + * Latency tuning is only supported for AAudio. + * + * @return OK or negative error, ErrorUnimplemented for OpenSL ES + */ + Result tune(); + + /** + * This may be called from another thread. Then tune() will call reset(), + * which will lower the latency to the minimum and then allow it to rise back up + * if there are glitches. + * + * This is typically called in response to a user decision to minimize latency. In other words, + * call this from a button handler. + */ + void requestReset(); + + /** + * @return true if the audio stream's buffer size is at the maximum value. If no maximum value + * was specified when constructing the LatencyTuner then the value of + * stream->getBufferCapacityInFrames is used + */ + bool isAtMaximumBufferSize(); + + /** + * Set the minimum bufferSize in frames that is used when the tuner is reset. + * You may wish to call requestReset() after calling this. + * @param bufferSize + */ + void setMinimumBufferSize(int32_t bufferSize) { + mMinimumBufferSize = bufferSize; + } + + int32_t getMinimumBufferSize() const { + return mMinimumBufferSize; + } + + /** + * Set the amount the bufferSize will be incremented while tuning. + * By default, this will be one burst. + * + * Note that AAudio will quantize the buffer size to a multiple of the burstSize. + * So the final buffer sizes may not be a multiple of this increment. + * + * @param sizeIncrement + */ + void setBufferSizeIncrement(int32_t sizeIncrement) { + mBufferSizeIncrement = sizeIncrement; + } + + int32_t getBufferSizeIncrement() const { + return mBufferSizeIncrement; + } + +private: + + /** + * Drop the latency down to the minimum and then let it rise back up. + * This is useful if a glitch caused the latency to increase and it hasn't gone back down. + * + * This should only be called in the same thread as tune(). + */ + void reset(); + + enum class State { + Idle, + Active, + AtMax, + Unsupported + } ; + + // arbitrary number of calls to wait before bumping up the latency + static constexpr int32_t kIdleCount = 8; + static constexpr int32_t kDefaultNumBursts = 2; + + AudioStream &mStream; + State mState = State::Idle; + int32_t mMaxBufferSize = 0; + int32_t mPreviousXRuns = 0; + int32_t mIdleCountDown = 0; + int32_t mMinimumBufferSize; + int32_t mBufferSizeIncrement; + std::atomic mLatencyTriggerRequests{0}; // TODO user atomic requester from AAudio + std::atomic mLatencyTriggerResponses{0}; +}; + +} // namespace oboe + +#endif // OBOE_LATENCY_TUNER_ diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_OboeExtensions_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_OboeExtensions_android.h new file mode 100644 index 0000000..006edd2 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_OboeExtensions_android.h @@ -0,0 +1,69 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_EXTENSIONS_ +#define OBOE_EXTENSIONS_ + +#include + +#include "oboe_oboe_Definitions_android.h" +#include "oboe_oboe_AudioStream_android.h" + +namespace oboe { + +/** + * The definitions below are only for testing. + * They are not recommended for use in an application. + * They may change or be removed at any time. + */ +class OboeExtensions { +public: + + /** + * @returns true if the device supports AAudio MMAP + */ + static bool isMMapSupported(); + + /** + * @returns true if the AAudio MMAP data path can be selected + */ + static bool isMMapEnabled(); + + /** + * Controls whether the AAudio MMAP data path can be selected when opening a stream. + * It has no effect after the stream has been opened. + * It only affects the application that calls it. Other apps are not affected. + * + * @param enabled + * @return 0 or a negative error code + */ + static int32_t setMMapEnabled(bool enabled); + + /** + * @param oboeStream + * @return true if the AAudio MMAP data path is used on the stream + */ + static bool isMMapUsed(oboe::AudioStream *oboeStream); + + /** + * @returns true if partial data callback is supported. + */ + static bool isPartialDataCallbackSupported(); +}; + +} // namespace oboe + +#endif // OBOE_LATENCY_TUNER_ diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_Oboe_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_Oboe_android.h new file mode 100644 index 0000000..729c78b --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_Oboe_android.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_OBOE_H +#define OBOE_OBOE_H + +/** + * \mainpage API reference + * + * See our guide on Github + * for a guide on Oboe. + * + * Click the classes tab to see the reference for various Oboe functions. + * + */ + +#include "oboe_oboe_Definitions_android.h" +#include "oboe_oboe_ResultWithValue_android.h" +#include "oboe_oboe_LatencyTuner_android.h" +#include "oboe_oboe_AudioStream_android.h" +#include "oboe_oboe_AudioStreamBase_android.h" +#include "oboe_oboe_AudioStreamBuilder_android.h" +#include "oboe_oboe_Utilities_android.h" +#include "oboe_oboe_Version_android.h" +#include "oboe_oboe_StabilizedCallback_android.h" +#include "oboe_oboe_FifoBuffer_android.h" +#include "oboe_oboe_OboeExtensions_android.h" +#include "oboe_oboe_FullDuplexStream_android.h" +#include "oboe_oboe_AudioClock_android.h" + +#endif //OBOE_OBOE_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_ResultWithValue_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_ResultWithValue_android.h new file mode 100644 index 0000000..26f0908 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_ResultWithValue_android.h @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_RESULT_WITH_VALUE_H +#define OBOE_RESULT_WITH_VALUE_H + +#include "oboe_oboe_Definitions_android.h" +#include +#include + +namespace oboe { + +/** + * A ResultWithValue can store both the result of an operation (either OK or an error) and a value. + * + * It has been designed for cases where the caller needs to know whether an operation succeeded and, + * if it did, a value which was obtained during the operation. + * + * For example, when reading from a stream the caller needs to know the result of the read operation + * and, if it was successful, how many frames were read. Note that ResultWithValue can be evaluated + * as a boolean so it's simple to check whether the result is OK. + * + * + * ResultWithValue resultOfRead = myStream.read(&buffer, numFrames, timeoutNanoseconds); + * + * if (resultOfRead) { + * LOGD("Frames read: %d", resultOfRead.value()); + * } else { + * LOGD("Error reading from stream: %s", resultOfRead.error()); + * } + * + */ +template +class ResultWithValue { +public: + + /** + * Construct a ResultWithValue containing an error result. + * + * @param error The error + */ + ResultWithValue(oboe::Result error) + : mValue{} + , mError(error) {} + + /** + * Construct a ResultWithValue containing an OK result and a value. + * + * @param value the value to store + */ + explicit ResultWithValue(T value) + : mValue(value) + , mError(oboe::Result::OK) {} + + ResultWithValue(T value, oboe::Result error) + : mValue(value) + , mError(error) {} + + /** + * Get the result. + * + * @return the result + */ + oboe::Result error() const { + return mError; + } + + /** + * Get the value + * @return + */ + T value() const { + return mValue; + } + + /** + * @return true if OK + */ + explicit operator bool() const { return mError == oboe::Result::OK; } + + /** + * Quick way to check for an error. + * + * The caller could write something like this: + * + * if (!result) { printf("Got error %s\n", convertToText(result.error())); } + * + * + * @return true if an error occurred + */ + bool operator !() const { return mError != oboe::Result::OK; } + + /** + * Implicitly convert to a Result. This enables easy comparison with Result values. Example: + * + * + * ResultWithValue result = openStream(); + * if (result == Result::ErrorNoMemory){ // tell user they're out of memory } + * + */ + operator Result() const { + return mError; + } + + /** + * Create a ResultWithValue from a number. If the number is positive the ResultWithValue will + * have a result of Result::OK and the value will contain the number. If the number is negative + * the result will be obtained from the negative number (numeric error codes can be found in + * AAudio.h) and the value will be null. + * + */ + static ResultWithValue createBasedOnSign(T numericResult){ + + // Ensure that the type is either an integer or float + static_assert(std::is_arithmetic::value, + "createBasedOnSign can only be called for numeric types (int or float)"); + + if (numericResult >= 0){ + return ResultWithValue(numericResult); + } else { + return ResultWithValue(static_cast(numericResult)); + } + } + +private: + const T mValue; + const oboe::Result mError; +}; + +/** + * If the result is `OK` then return the value, otherwise return a human-readable error message. + */ +template +std::ostream& operator<<(std::ostream &strm, const ResultWithValue &result) { + if (!result) { + strm << convertToText(result.error()); + } else { + strm << result.value(); + } + return strm; +} + +} // namespace oboe + + +#endif //OBOE_RESULT_WITH_VALUE_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_StabilizedCallback_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_StabilizedCallback_android.h new file mode 100644 index 0000000..1e84343 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_StabilizedCallback_android.h @@ -0,0 +1,75 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_STABILIZEDCALLBACK_H +#define OBOE_STABILIZEDCALLBACK_H + +#include +#include "oboe_oboe_AudioStream_android.h" + +namespace oboe { + +class StabilizedCallback : public AudioStreamCallback { + +public: + explicit StabilizedCallback(AudioStreamCallback *callback); + + DataCallbackResult + onAudioReady(AudioStream *oboeStream, void *audioData, int32_t numFrames) override; + + void onErrorBeforeClose(AudioStream *oboeStream, Result error) override { + return mCallback->onErrorBeforeClose(oboeStream, error); + } + + void onErrorAfterClose(AudioStream *oboeStream, Result error) override { + + // Reset all fields now that the stream has been closed + mFrameCount = 0; + mEpochTimeNanos = 0; + mOpsPerNano = 1; + return mCallback->onErrorAfterClose(oboeStream, error); + } + +private: + + AudioStreamCallback *mCallback = nullptr; + int64_t mFrameCount = 0; + int64_t mEpochTimeNanos = 0; + double mOpsPerNano = 1; + + void generateLoad(int64_t durationNanos); +}; + +/** + * cpu_relax is an architecture specific method of telling the CPU that you don't want it to + * do much work. asm volatile keeps the compiler from optimising these instructions out. + */ +#if defined(__i386__) || defined(__x86_64__) +#define cpu_relax() asm volatile("rep; nop" ::: "memory"); + +#elif defined(__arm__) || defined(__mips__) || defined(__riscv) + #define cpu_relax() asm volatile("":::"memory") + +#elif defined(__aarch64__) +#define cpu_relax() asm volatile("yield" ::: "memory") + +#else +#error "cpu_relax is not defined for this architecture" +#endif + +} + +#endif //OBOE_STABILIZEDCALLBACK_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_Utilities_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_Utilities_android.h new file mode 100644 index 0000000..06868de --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_Utilities_android.h @@ -0,0 +1,103 @@ +/* + * Copyright 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_UTILITIES_H +#define OBOE_UTILITIES_H + +#include +#include +#include +#include "oboe_oboe_Definitions_android.h" + +namespace oboe { + +/** + * Convert an array of floats to an array of 16-bit integers. + * + * @param source the input array. + * @param destination the output array. + * @param numSamples the number of values to convert. + */ +void convertFloatToPcm16(const float *source, int16_t *destination, int32_t numSamples); + +/** + * Convert an array of 16-bit integers to an array of floats. + * + * @param source the input array. + * @param destination the output array. + * @param numSamples the number of values to convert. + */ +void convertPcm16ToFloat(const int16_t *source, float *destination, int32_t numSamples); + +/** + * @return the size of a sample of the given format in bytes or 0 if format is invalid + */ +int32_t convertFormatToSizeInBytes(AudioFormat format); + +/** + * The text is the ASCII symbol corresponding to the supplied Oboe enum value, + * or an English message saying the value is unrecognized. + * This is intended for developers to use when debugging. + * It is not for displaying to users. + * + * @param input object to convert from. @see common/Utilities.cpp for concrete implementations + * @return text representation of an Oboe enum value. There is no need to call free on this. + */ +template +const char * convertToText(FromType input); + +/** + * @param name + * @return the value of a named system property in a string or empty string + */ +std::string getPropertyString(const char * name); + +/** + * @param name + * @param defaultValue + * @return integer value associated with a property or the default value + */ +int getPropertyInteger(const char * name, int defaultValue); + +/** + * Return the version of the SDK that is currently running. + * + * For example, on Android, this would return 27 for Oreo 8.1. + * If the version number cannot be determined then this will return -1. + * + * @return version number or -1 + */ +int getSdkVersion(); + +/** + * Returns whether a device is on a pre-release SDK that is at least the specified codename + * version. + * + * @param codename the code name to verify. + * @return boolean of whether the device is on a pre-release SDK and is at least the specified + * codename + */ +bool isAtLeastPreReleaseCodename(const std::string& codename); + +int getChannelCountFromChannelMask(ChannelMask channelMask); + +bool isCompressedFormat(AudioFormat format); + +std::string toString(const PlaybackParameters& parameters); + +} // namespace oboe + +#endif //OBOE_UTILITIES_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_Version_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_Version_android.h new file mode 100644 index 0000000..673da73 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_oboe_Version_android.h @@ -0,0 +1,92 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_VERSIONINFO_H +#define OBOE_VERSIONINFO_H + +#include + +/** + * A note on use of preprocessor defines: + * + * This is one of the few times when it's suitable to use preprocessor defines rather than constexpr + * Why? Because C++11 requires a lot of boilerplate code to convert integers into compile-time + * string literals. The preprocessor, despite it's lack of type checking, is more suited to the task + * + * See: https://stackoverflow.com/questions/6713420/c-convert-integer-to-string-at-compile-time/26824971#26824971 + * + */ + +// Type: 8-bit unsigned int. Min value: 0 Max value: 255. See below for description. +#define OBOE_VERSION_MAJOR 1 + +// Type: 8-bit unsigned int. Min value: 0 Max value: 255. See below for description. +#define OBOE_VERSION_MINOR 10 + +// Type: 16-bit unsigned int. Min value: 0 Max value: 65535. See below for description. +#define OBOE_VERSION_PATCH 0 + +#define OBOE_STRINGIFY(x) #x +#define OBOE_TOSTRING(x) OBOE_STRINGIFY(x) + +// Type: String literal. See below for description. +#define OBOE_VERSION_TEXT \ + OBOE_TOSTRING(OBOE_VERSION_MAJOR) "." \ + OBOE_TOSTRING(OBOE_VERSION_MINOR) "." \ + OBOE_TOSTRING(OBOE_VERSION_PATCH) + +// Type: 32-bit unsigned int. See below for description. +#define OBOE_VERSION_NUMBER ((OBOE_VERSION_MAJOR << 24) | (OBOE_VERSION_MINOR << 16) | OBOE_VERSION_PATCH) + +namespace oboe { + +const char * getVersionText(); + +/** + * Oboe versioning object + */ +struct Version { + /** + * This is incremented when we make breaking API changes. Based loosely on https://semver.org/. + */ + static constexpr uint8_t Major = OBOE_VERSION_MAJOR; + + /** + * This is incremented when we add backwards compatible functionality. Or set to zero when MAJOR is + * incremented. + */ + static constexpr uint8_t Minor = OBOE_VERSION_MINOR; + + /** + * This is incremented when we make backwards compatible bug fixes. Or set to zero when MINOR is + * incremented. + */ + static constexpr uint16_t Patch = OBOE_VERSION_PATCH; + + /** + * Version string in the form MAJOR.MINOR.PATCH. + */ + static constexpr const char * Text = OBOE_VERSION_TEXT; + + /** + * Integer representation of the current Oboe library version. This will always increase when the + * version number changes so can be compared using integer comparison. + */ + static constexpr uint32_t Number = OBOE_VERSION_NUMBER; +}; + +} // namespace oboe +#endif //OBOE_VERSIONINFO_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_AudioInputStreamOpenSLES_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_AudioInputStreamOpenSLES_android.cpp new file mode 100644 index 0000000..4f6ebcd --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_AudioInputStreamOpenSLES_android.cpp @@ -0,0 +1,359 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "oboe_common_OboeDebug_android.h" +#include "oboe_oboe_AudioStreamBuilder_android.h" +#include "oboe_opensles_AudioInputStreamOpenSLES_android.h" +#include "oboe_opensles_AudioStreamOpenSLES_android.h" +#include "oboe_opensles_OpenSLESUtilities_android.h" + +using namespace oboe; + +static SLuint32 OpenSLES_convertInputPreset(InputPreset oboePreset) { + SLuint32 openslPreset = SL_ANDROID_RECORDING_PRESET_NONE; + switch(oboePreset) { + case InputPreset::Generic: + openslPreset = SL_ANDROID_RECORDING_PRESET_GENERIC; + break; + case InputPreset::Camcorder: + openslPreset = SL_ANDROID_RECORDING_PRESET_CAMCORDER; + break; + case InputPreset::VoiceRecognition: + case InputPreset::VoicePerformance: + openslPreset = SL_ANDROID_RECORDING_PRESET_VOICE_RECOGNITION; + break; + case InputPreset::VoiceCommunication: + openslPreset = SL_ANDROID_RECORDING_PRESET_VOICE_COMMUNICATION; + break; + case InputPreset::Unprocessed: + openslPreset = SL_ANDROID_RECORDING_PRESET_UNPROCESSED; + break; + default: + break; + } + return openslPreset; +} + +AudioInputStreamOpenSLES::AudioInputStreamOpenSLES(const AudioStreamBuilder &builder) + : AudioStreamOpenSLES(builder) { +} + +AudioInputStreamOpenSLES::~AudioInputStreamOpenSLES() { +} + +// Calculate masks specific to INPUT streams. +SLuint32 AudioInputStreamOpenSLES::channelCountToChannelMask(int channelCount) const { + // Derived from internal sles_channel_in_mask_from_count(chanCount); + // in "frameworks/wilhelm/src/android/channels.cpp". + // Yes, it seems strange to use SPEAKER constants to describe inputs. + // But that is how OpenSL ES does it internally. + switch (channelCount) { + case 1: + return SL_SPEAKER_FRONT_LEFT; + case 2: + return SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT; + default: + return channelCountToChannelMaskDefault(channelCount); + } +} + +Result AudioInputStreamOpenSLES::open() { + logUnsupportedAttributes(); + + SLAndroidConfigurationItf configItf = nullptr; + + if (getSdkVersion() < __ANDROID_API_M__ && mFormat == AudioFormat::Float){ + // TODO: Allow floating point format on API <23 using float->int16 converter + return Result::ErrorInvalidFormat; + } + + // If audio format is unspecified then choose a suitable default. + // API 23+: FLOAT + // API <23: INT16 + if (mFormat == AudioFormat::Unspecified){ + mFormat = (getSdkVersion() < __ANDROID_API_M__) ? + AudioFormat::I16 : AudioFormat::Float; + } + + Result oboeResult = AudioStreamOpenSLES::open(); + if (Result::OK != oboeResult) return oboeResult; + + SLuint32 bitsPerSample = static_cast(getBytesPerSample() * kBitsPerByte); + + // configure audio sink + mBufferQueueLength = calculateOptimalBufferQueueLength(); + SLDataLocator_AndroidSimpleBufferQueue loc_bufq = { + SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, // locatorType + static_cast(mBufferQueueLength)}; // numBuffers + + // Define the audio data format. + SLDataFormat_PCM format_pcm = { + SL_DATAFORMAT_PCM, // formatType + static_cast(mChannelCount), // numChannels + static_cast(mSampleRate * kMillisPerSecond), // milliSamplesPerSec + bitsPerSample, // mBitsPerSample + bitsPerSample, // containerSize; + channelCountToChannelMask(mChannelCount), // channelMask + getDefaultByteOrder(), + }; + + SLDataSink audioSink = {&loc_bufq, &format_pcm}; + + /** + * API 23 (Marshmallow) introduced support for floating-point data representation and an + * extended data format type: SLAndroidDataFormat_PCM_EX for recording streams (playback streams + * got this in API 21). If running on API 23+ use this newer format type, creating it from our + * original format. + */ + SLAndroidDataFormat_PCM_EX format_pcm_ex; + if (getSdkVersion() >= __ANDROID_API_M__) { + SLuint32 representation = OpenSLES_ConvertFormatToRepresentation(getFormat()); + // Fill in the format structure. + format_pcm_ex = OpenSLES_createExtendedFormat(format_pcm, representation); + // Use in place of the previous format. + audioSink.pFormat = &format_pcm_ex; + } + + + // configure audio source + SLDataLocator_IODevice loc_dev = {SL_DATALOCATOR_IODEVICE, + SL_IODEVICE_AUDIOINPUT, + SL_DEFAULTDEVICEID_AUDIOINPUT, + NULL}; + SLDataSource audioSrc = {&loc_dev, NULL}; + + SLresult result = EngineOpenSLES::getInstance().createAudioRecorder(&mObjectInterface, + &audioSrc, + &audioSink); + + if (SL_RESULT_SUCCESS != result) { + LOGE("createAudioRecorder() result:%s", getSLErrStr(result)); + goto error; + } + + // Configure the stream. + result = (*mObjectInterface)->GetInterface(mObjectInterface, + EngineOpenSLES::getInstance().getIidAndroidConfiguration(), + &configItf); + + if (SL_RESULT_SUCCESS != result) { + LOGW("%s() GetInterface(SL_IID_ANDROIDCONFIGURATION) failed with %s", + __func__, getSLErrStr(result)); + } else { + if (getInputPreset() == InputPreset::VoicePerformance) { + LOGD("OpenSL ES does not support InputPreset::VoicePerformance. Use VoiceRecognition."); + mInputPreset = InputPreset::VoiceRecognition; + } + SLuint32 presetValue = OpenSLES_convertInputPreset(getInputPreset()); + result = (*configItf)->SetConfiguration(configItf, + SL_ANDROID_KEY_RECORDING_PRESET, + &presetValue, + sizeof(SLuint32)); + if (SL_RESULT_SUCCESS != result + && presetValue != SL_ANDROID_RECORDING_PRESET_VOICE_RECOGNITION) { + presetValue = SL_ANDROID_RECORDING_PRESET_VOICE_RECOGNITION; + LOGD("Setting InputPreset %d failed. Using VoiceRecognition instead.", getInputPreset()); + mInputPreset = InputPreset::VoiceRecognition; + (*configItf)->SetConfiguration(configItf, + SL_ANDROID_KEY_RECORDING_PRESET, + &presetValue, + sizeof(SLuint32)); + } + + result = configurePerformanceMode(configItf); + if (SL_RESULT_SUCCESS != result) { + goto error; + } + } + + result = (*mObjectInterface)->Realize(mObjectInterface, SL_BOOLEAN_FALSE); + if (SL_RESULT_SUCCESS != result) { + LOGE("Realize recorder object result:%s", getSLErrStr(result)); + goto error; + } + + result = (*mObjectInterface)->GetInterface(mObjectInterface, + EngineOpenSLES::getInstance().getIidRecord(), + &mRecordInterface); + if (SL_RESULT_SUCCESS != result) { + LOGE("GetInterface RECORD result:%s", getSLErrStr(result)); + goto error; + } + + result = finishCommonOpen(configItf); + if (SL_RESULT_SUCCESS != result) { + goto error; + } + + setState(StreamState::Open); + return Result::OK; + +error: + close(); // Clean up various OpenSL objects and prevent resource leaks. + return Result::ErrorInternal; // TODO convert error from SLES to OBOE +} + +Result AudioInputStreamOpenSLES::close() { + LOGD("AudioInputStreamOpenSLES::%s()", __func__); + std::lock_guard lock(mLock); + Result result = Result::OK; + if (getState() == StreamState::Closed) { + result = Result::ErrorClosed; + } else { + (void) requestStop_l(); + if (OboeGlobals::areWorkaroundsEnabled()) { + sleepBeforeClose(); + } + // invalidate any interfaces + mRecordInterface = nullptr; + result = AudioStreamOpenSLES::close_l(); + } + return result; +} + +Result AudioInputStreamOpenSLES::setRecordState_l(SLuint32 newState) { + LOGD("AudioInputStreamOpenSLES::%s(%u)", __func__, newState); + Result result = Result::OK; + + if (mRecordInterface == nullptr) { + LOGW("AudioInputStreamOpenSLES::%s() mRecordInterface is null", __func__); + return Result::ErrorInvalidState; + } + SLresult slResult = (*mRecordInterface)->SetRecordState(mRecordInterface, newState); + //LOGD("AudioInputStreamOpenSLES::%s(%u) returned %u", __func__, newState, slResult); + if (SL_RESULT_SUCCESS != slResult) { + LOGE("AudioInputStreamOpenSLES::%s(%u) returned error %s", + __func__, newState, getSLErrStr(slResult)); + result = Result::ErrorInternal; // TODO review + } + return result; +} + +Result AudioInputStreamOpenSLES::requestStart() { + LOGD("AudioInputStreamOpenSLES(): %s() called", __func__); + std::lock_guard lock(mLock); + StreamState initialState = getState(); + switch (initialState) { + case StreamState::Starting: + case StreamState::Started: + return Result::OK; + case StreamState::Closed: + return Result::ErrorClosed; + default: + break; + } + + // We use a callback if the user requests one + // OR if we have an internal callback to fill the blocking IO buffer. + setDataCallbackEnabled(true); + + setState(StreamState::Starting); + + closePerformanceHint(); + + if (getBufferDepth(mSimpleBufferQueueInterface) == 0) { + // Enqueue the first buffer to start the streaming. + // This does not call the callback function. + enqueueCallbackBuffer(mSimpleBufferQueueInterface); + } + + Result result = setRecordState_l(SL_RECORDSTATE_RECORDING); + if (result == Result::OK) { + setState(StreamState::Started); + } else { + setState(initialState); + } + return result; +} + + +Result AudioInputStreamOpenSLES::requestPause() { + LOGW("AudioInputStreamOpenSLES::%s() is intentionally not implemented for input " + "streams", __func__); + return Result::ErrorUnimplemented; // Matches AAudio behavior. +} + +Result AudioInputStreamOpenSLES::requestFlush() { + LOGW("AudioInputStreamOpenSLES::%s() is intentionally not implemented for input " + "streams", __func__); + return Result::ErrorUnimplemented; // Matches AAudio behavior. +} + +Result AudioInputStreamOpenSLES::requestStop() { + LOGD("AudioInputStreamOpenSLES(): %s() called", __func__); + std::lock_guard lock(mLock); + return requestStop_l(); +} + +// Call under mLock +Result AudioInputStreamOpenSLES::requestStop_l() { + StreamState initialState = getState(); + switch (initialState) { + case StreamState::Stopping: + case StreamState::Stopped: + return Result::OK; + case StreamState::Uninitialized: + case StreamState::Closed: + return Result::ErrorClosed; + default: + break; + } + + setState(StreamState::Stopping); + + Result result = setRecordState_l(SL_RECORDSTATE_STOPPED); + if (result == Result::OK) { + mPositionMillis.reset32(); // OpenSL ES resets its millisecond position when stopped. + setState(StreamState::Stopped); + } else { + setState(initialState); + } + return result; +} + +void AudioInputStreamOpenSLES::updateFramesWritten() { + if (usingFIFO()) { + AudioStreamBuffered::updateFramesWritten(); + } else { + mFramesWritten = getFramesProcessedByServer(); + } +} + +Result AudioInputStreamOpenSLES::updateServiceFrameCounter() { + Result result = Result::OK; + // Avoid deadlock if another thread is trying to stop or close this stream + // and this is being called from a callback. + if (mLock.try_lock()) { + + if (mRecordInterface == nullptr) { + mLock.unlock(); + return Result::ErrorNull; + } + SLmillisecond msec = 0; + SLresult slResult = (*mRecordInterface)->GetPosition(mRecordInterface, &msec); + if (SL_RESULT_SUCCESS != slResult) { + LOGW("%s(): GetPosition() returned %s", __func__, getSLErrStr(slResult)); + // set result based on SLresult + result = Result::ErrorInternal; + } else { + mPositionMillis.update32(msec); + } + mLock.unlock(); + } + return result; +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_AudioInputStreamOpenSLES_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_AudioInputStreamOpenSLES_android.h new file mode 100644 index 0000000..7ef6fcb --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_AudioInputStreamOpenSLES_android.h @@ -0,0 +1,64 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AUDIO_INPUT_STREAM_OPENSL_ES_H_ +#define AUDIO_INPUT_STREAM_OPENSL_ES_H_ + + +#include "oboe_oboe_Oboe_android.h" +#include "oboe_opensles_EngineOpenSLES_android.h" +#include "oboe_opensles_AudioStreamOpenSLES_android.h" + +namespace oboe { + +/** + * INTERNAL USE ONLY + */ + +class AudioInputStreamOpenSLES : public AudioStreamOpenSLES { +public: + AudioInputStreamOpenSLES(); + explicit AudioInputStreamOpenSLES(const AudioStreamBuilder &builder); + + virtual ~AudioInputStreamOpenSLES(); + + Result open() override; + Result close() override; + + Result requestStart() override; + Result requestPause() override; + Result requestFlush() override; + Result requestStop() override; + +protected: + Result requestStop_l(); + + Result updateServiceFrameCounter() override; + + void updateFramesWritten() override; + +private: + + SLuint32 channelCountToChannelMask(int chanCount) const; + + Result setRecordState_l(SLuint32 newState); + + SLRecordItf mRecordInterface = nullptr; +}; + +} // namespace oboe + +#endif //AUDIO_INPUT_STREAM_OPENSL_ES_H_ diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_AudioOutputStreamOpenSLES_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_AudioOutputStreamOpenSLES_android.cpp new file mode 100644 index 0000000..4c57446 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_AudioOutputStreamOpenSLES_android.cpp @@ -0,0 +1,457 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "oboe_common_OboeDebug_android.h" +#include "oboe_oboe_AudioClock_android.h" +#include "oboe_oboe_AudioStreamBuilder_android.h" +#include "oboe_opensles_AudioOutputStreamOpenSLES_android.h" +#include "oboe_opensles_AudioStreamOpenSLES_android.h" +#include "oboe_opensles_OpenSLESUtilities_android.h" +#include "oboe_opensles_OutputMixerOpenSLES_android.h" + +using namespace oboe; + +static SLuint32 OpenSLES_convertOutputUsage(Usage oboeUsage) { + SLuint32 openslStream; + switch(oboeUsage) { + case Usage::Media: + case Usage::Game: + openslStream = SL_ANDROID_STREAM_MEDIA; + break; + case Usage::VoiceCommunication: + case Usage::VoiceCommunicationSignalling: + openslStream = SL_ANDROID_STREAM_VOICE; + break; + case Usage::Alarm: + openslStream = SL_ANDROID_STREAM_ALARM; + break; + case Usage::Notification: + case Usage::NotificationEvent: + openslStream = SL_ANDROID_STREAM_NOTIFICATION; + break; + case Usage::NotificationRingtone: + openslStream = SL_ANDROID_STREAM_RING; + break; + case Usage::AssistanceAccessibility: + case Usage::AssistanceNavigationGuidance: + case Usage::AssistanceSonification: + case Usage::Assistant: + default: + openslStream = SL_ANDROID_STREAM_SYSTEM; + break; + } + return openslStream; +} + +AudioOutputStreamOpenSLES::AudioOutputStreamOpenSLES(const AudioStreamBuilder &builder) + : AudioStreamOpenSLES(builder) { +} + +// These will wind up in +constexpr int SL_ANDROID_SPEAKER_STEREO = (SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT); + +constexpr int SL_ANDROID_SPEAKER_QUAD = (SL_ANDROID_SPEAKER_STEREO + | SL_SPEAKER_BACK_LEFT | SL_SPEAKER_BACK_RIGHT); + +constexpr int SL_ANDROID_SPEAKER_5DOT1 = (SL_ANDROID_SPEAKER_QUAD + | SL_SPEAKER_FRONT_CENTER | SL_SPEAKER_LOW_FREQUENCY); + +constexpr int SL_ANDROID_SPEAKER_7DOT1 = (SL_ANDROID_SPEAKER_5DOT1 | SL_SPEAKER_SIDE_LEFT + | SL_SPEAKER_SIDE_RIGHT); + +SLuint32 AudioOutputStreamOpenSLES::channelCountToChannelMask(int channelCount) const { + SLuint32 channelMask = 0; + + switch (channelCount) { + case 1: + channelMask = SL_SPEAKER_FRONT_CENTER; + break; + + case 2: + channelMask = SL_ANDROID_SPEAKER_STEREO; + break; + + case 4: // Quad + channelMask = SL_ANDROID_SPEAKER_QUAD; + break; + + case 6: // 5.1 + channelMask = SL_ANDROID_SPEAKER_5DOT1; + break; + + case 8: // 7.1 + channelMask = SL_ANDROID_SPEAKER_7DOT1; + break; + + default: + channelMask = channelCountToChannelMaskDefault(channelCount); + break; + } + return channelMask; +} + +Result AudioOutputStreamOpenSLES::open() { + logUnsupportedAttributes(); + + SLAndroidConfigurationItf configItf = nullptr; + + + if (getSdkVersion() < __ANDROID_API_L__ && mFormat == AudioFormat::Float){ + // TODO: Allow floating point format on API <21 using float->int16 converter + return Result::ErrorInvalidFormat; + } + + // If audio format is unspecified then choose a suitable default. + // API 21+: FLOAT + // API <21: INT16 + if (mFormat == AudioFormat::Unspecified){ + mFormat = (getSdkVersion() < __ANDROID_API_L__) ? + AudioFormat::I16 : AudioFormat::Float; + } + + Result oboeResult = AudioStreamOpenSLES::open(); + if (Result::OK != oboeResult) return oboeResult; + + SLresult result = OutputMixerOpenSL::getInstance().open(); + if (SL_RESULT_SUCCESS != result) { + AudioStreamOpenSLES::close(); + return Result::ErrorInternal; + } + + SLuint32 bitsPerSample = static_cast(getBytesPerSample() * kBitsPerByte); + + // configure audio source + mBufferQueueLength = calculateOptimalBufferQueueLength(); + SLDataLocator_AndroidSimpleBufferQueue loc_bufq = { + SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, // locatorType + static_cast(mBufferQueueLength)}; // numBuffers + + // Define the audio data format. + SLDataFormat_PCM format_pcm = { + SL_DATAFORMAT_PCM, // formatType + static_cast(mChannelCount), // numChannels + static_cast(mSampleRate * kMillisPerSecond), // milliSamplesPerSec + bitsPerSample, // mBitsPerSample + bitsPerSample, // containerSize; + channelCountToChannelMask(mChannelCount), // channelMask + getDefaultByteOrder(), + }; + + SLDataSource audioSrc = {&loc_bufq, &format_pcm}; + + /** + * API 21 (Lollipop) introduced support for floating-point data representation and an extended + * data format type: SLAndroidDataFormat_PCM_EX. If running on API 21+ use this newer format + * type, creating it from our original format. + */ + SLAndroidDataFormat_PCM_EX format_pcm_ex; + if (getSdkVersion() >= __ANDROID_API_L__) { + SLuint32 representation = OpenSLES_ConvertFormatToRepresentation(getFormat()); + // Fill in the format structure. + format_pcm_ex = OpenSLES_createExtendedFormat(format_pcm, representation); + // Use in place of the previous format. + audioSrc.pFormat = &format_pcm_ex; + } + + result = OutputMixerOpenSL::getInstance().createAudioPlayer(&mObjectInterface, + &audioSrc); + if (SL_RESULT_SUCCESS != result) { + LOGE("createAudioPlayer() result:%s", getSLErrStr(result)); + goto error; + } + + // Configure the stream. + result = (*mObjectInterface)->GetInterface(mObjectInterface, + EngineOpenSLES::getInstance().getIidAndroidConfiguration(), + (void *)&configItf); + if (SL_RESULT_SUCCESS != result) { + LOGW("%s() GetInterface(SL_IID_ANDROIDCONFIGURATION) failed with %s", + __func__, getSLErrStr(result)); + } else { + result = configurePerformanceMode(configItf); + if (SL_RESULT_SUCCESS != result) { + goto error; + } + + SLuint32 presetValue = OpenSLES_convertOutputUsage(getUsage()); + result = (*configItf)->SetConfiguration(configItf, + SL_ANDROID_KEY_STREAM_TYPE, + &presetValue, + sizeof(presetValue)); + if (SL_RESULT_SUCCESS != result) { + goto error; + } + } + + result = (*mObjectInterface)->Realize(mObjectInterface, SL_BOOLEAN_FALSE); + if (SL_RESULT_SUCCESS != result) { + LOGE("Realize player object result:%s", getSLErrStr(result)); + goto error; + } + + result = (*mObjectInterface)->GetInterface(mObjectInterface, + EngineOpenSLES::getInstance().getIidPlay(), + &mPlayInterface); + if (SL_RESULT_SUCCESS != result) { + LOGE("GetInterface PLAY result:%s", getSLErrStr(result)); + goto error; + } + + result = finishCommonOpen(configItf); + if (SL_RESULT_SUCCESS != result) { + goto error; + } + + setState(StreamState::Open); + return Result::OK; + +error: + close(); // Clean up various OpenSL objects and prevent resource leaks. + return Result::ErrorInternal; // TODO convert error from SLES to OBOE +} + +Result AudioOutputStreamOpenSLES::onAfterDestroy() { + OutputMixerOpenSL::getInstance().close(); + return Result::OK; +} + +Result AudioOutputStreamOpenSLES::close() { + LOGD("AudioOutputStreamOpenSLES::%s()", __func__); + std::lock_guard lock(mLock); + Result result = Result::OK; + if (getState() == StreamState::Closed) { + result = Result::ErrorClosed; + } else { + (void) requestPause_l(); + if (OboeGlobals::areWorkaroundsEnabled()) { + sleepBeforeClose(); + } + // invalidate any interfaces + mPlayInterface = nullptr; + result = AudioStreamOpenSLES::close_l(); + } + return result; +} + +Result AudioOutputStreamOpenSLES::setPlayState_l(SLuint32 newState) { + LOGD("AudioOutputStreamOpenSLES::%s(%d) called", __func__, newState); + Result result = Result::OK; + + if (mPlayInterface == nullptr){ + LOGE("AudioOutputStreamOpenSLES::%s() mPlayInterface is null", __func__); + return Result::ErrorInvalidState; + } + + SLresult slResult = (*mPlayInterface)->SetPlayState(mPlayInterface, newState); + if (SL_RESULT_SUCCESS != slResult) { + LOGW("AudioOutputStreamOpenSLES(): %s() returned %s", __func__, getSLErrStr(slResult)); + result = Result::ErrorInternal; // TODO convert slResult to Result::Error + } + return result; +} + +Result AudioOutputStreamOpenSLES::requestStart() { + LOGD("AudioOutputStreamOpenSLES::%s() called", __func__); + + mLock.lock(); + StreamState initialState = getState(); + switch (initialState) { + case StreamState::Starting: + case StreamState::Started: + mLock.unlock(); + return Result::OK; + case StreamState::Closed: + mLock.unlock(); + return Result::ErrorClosed; + default: + break; + } + + // We use a callback if the user requests one + // OR if we have an internal callback to read the blocking IO buffer. + setDataCallbackEnabled(true); + + setState(StreamState::Starting); + closePerformanceHint(); + + if (getBufferDepth(mSimpleBufferQueueInterface) == 0) { + // Enqueue the first buffer if needed to start the streaming. + // We may need to stop the current stream. + bool shouldStopStream = processBufferCallback(mSimpleBufferQueueInterface); + if (shouldStopStream) { + LOGD("Stopping the current stream."); + if (requestStop_l() != Result::OK) { + LOGW("Failed to flush the stream. Error %s", convertToText(flush())); + } + setState(initialState); + mLock.unlock(); + return Result::ErrorClosed; + } + } + + Result result = setPlayState_l(SL_PLAYSTATE_PLAYING); + if (result == Result::OK) { + setState(StreamState::Started); + mLock.unlock(); + } else { + setState(initialState); + mLock.unlock(); + } + return result; +} + +Result AudioOutputStreamOpenSLES::requestPause() { + LOGD("AudioOutputStreamOpenSLES::%s() called", __func__); + std::lock_guard lock(mLock); + return requestPause_l(); +} + +// Call under mLock +Result AudioOutputStreamOpenSLES::requestPause_l() { + StreamState initialState = getState(); + switch (initialState) { + case StreamState::Pausing: + case StreamState::Paused: + return Result::OK; + case StreamState::Uninitialized: + case StreamState::Closed: + return Result::ErrorClosed; + default: + break; + } + + setState(StreamState::Pausing); + Result result = setPlayState_l(SL_PLAYSTATE_PAUSED); + if (result == Result::OK) { + // Note that OpenSL ES does NOT reset its millisecond position when OUTPUT is paused. + int64_t framesWritten = getFramesWritten(); + if (framesWritten >= 0) { + setFramesRead(framesWritten); + } + setState(StreamState::Paused); + } else { + setState(initialState); + } + return result; +} + +/** + * Flush/clear the queue buffers + */ +Result AudioOutputStreamOpenSLES::requestFlush() { + std::lock_guard lock(mLock); + return requestFlush_l(); +} + +Result AudioOutputStreamOpenSLES::requestFlush_l() { + LOGD("AudioOutputStreamOpenSLES::%s() called", __func__); + if (getState() == StreamState::Closed) { + return Result::ErrorClosed; + } + + Result result = Result::OK; + if (mPlayInterface == nullptr || mSimpleBufferQueueInterface == nullptr) { + result = Result::ErrorInvalidState; + } else { + SLresult slResult = (*mSimpleBufferQueueInterface)->Clear(mSimpleBufferQueueInterface); + if (slResult != SL_RESULT_SUCCESS){ + LOGW("Failed to clear buffer queue. OpenSLES error: %s", getSLErrStr(slResult)); + result = Result::ErrorInternal; + } + } + return result; +} + +Result AudioOutputStreamOpenSLES::requestStop() { + std::lock_guard lock(mLock); + return requestStop_l(); +} + +Result AudioOutputStreamOpenSLES::requestStop_l() { + StreamState initialState = getState(); + LOGD("AudioOutputStreamOpenSLES::%s() called, initialState = %d", __func__, initialState); + switch (initialState) { + case StreamState::Stopping: + case StreamState::Stopped: + return Result::OK; + case StreamState::Uninitialized: + case StreamState::Closed: + return Result::ErrorClosed; + default: + break; + } + + setState(StreamState::Stopping); + + Result result = setPlayState_l(SL_PLAYSTATE_STOPPED); + if (result == Result::OK) { + + // Also clear the buffer queue so the old data won't be played if the stream is restarted. + // Call the _l function that expects to already be under a lock. + if (requestFlush_l() != Result::OK) { + LOGW("Failed to flush the stream. Error %s", convertToText(flush())); + } + + mPositionMillis.reset32(); // OpenSL ES resets its millisecond position when stopped. + int64_t framesWritten = getFramesWritten(); + if (framesWritten >= 0) { + setFramesRead(framesWritten); + } + setState(StreamState::Stopped); + } else { + setState(initialState); + } + return result; +} + +void AudioOutputStreamOpenSLES::setFramesRead(int64_t framesRead) { + int64_t millisWritten = framesRead * kMillisPerSecond / getSampleRate(); + mPositionMillis.set(millisWritten); +} + +void AudioOutputStreamOpenSLES::updateFramesRead() { + if (usingFIFO()) { + AudioStreamBuffered::updateFramesRead(); + } else { + mFramesRead = getFramesProcessedByServer(); + } +} + +Result AudioOutputStreamOpenSLES::updateServiceFrameCounter() { + Result result = Result::OK; + // Avoid deadlock if another thread is trying to stop or close this stream + // and this is being called from a callback. + if (mLock.try_lock()) { + + if (mPlayInterface == nullptr) { + mLock.unlock(); + return Result::ErrorNull; + } + SLmillisecond msec = 0; + SLresult slResult = (*mPlayInterface)->GetPosition(mPlayInterface, &msec); + if (SL_RESULT_SUCCESS != slResult) { + LOGW("%s(): GetPosition() returned %s", __func__, getSLErrStr(slResult)); + // set result based on SLresult + result = Result::ErrorInternal; + } else { + mPositionMillis.update32(msec); + } + mLock.unlock(); + } + return result; +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_AudioOutputStreamOpenSLES_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_AudioOutputStreamOpenSLES_android.h new file mode 100644 index 0000000..1ac7039 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_AudioOutputStreamOpenSLES_android.h @@ -0,0 +1,78 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AUDIO_OUTPUT_STREAM_OPENSL_ES_H_ +#define AUDIO_OUTPUT_STREAM_OPENSL_ES_H_ + + +#include "oboe_oboe_Oboe_android.h" +#include "oboe_opensles_EngineOpenSLES_android.h" +#include "oboe_opensles_AudioStreamOpenSLES_android.h" + +namespace oboe { + +/** + * INTERNAL USE ONLY + */ +class AudioOutputStreamOpenSLES : public AudioStreamOpenSLES { +public: + AudioOutputStreamOpenSLES(); + explicit AudioOutputStreamOpenSLES(const AudioStreamBuilder &builder); + + virtual ~AudioOutputStreamOpenSLES() = default; + + Result open() override; + Result close() override; + + Result requestStart() override; + Result requestPause() override; + Result requestFlush() override; + Result requestStop() override; + +protected: + Result requestPause_l(); + + void setFramesRead(int64_t framesRead); + + Result updateServiceFrameCounter() override; + + void updateFramesRead() override; + +private: + + SLuint32 channelCountToChannelMask(int chanCount) const; + + Result onAfterDestroy() override; + + Result requestFlush_l(); + + Result requestStop_l(); + + /** + * Set OpenSL ES PLAYSTATE. + * + * @param newState SL_PLAYSTATE_PAUSED, SL_PLAYSTATE_PLAYING, SL_PLAYSTATE_STOPPED + * @return + */ + Result setPlayState_l(SLuint32 newState); + + SLPlayItf mPlayInterface = nullptr; + +}; + +} // namespace oboe + +#endif //AUDIO_OUTPUT_STREAM_OPENSL_ES_H_ diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_AudioStreamBuffered_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_AudioStreamBuffered_android.cpp new file mode 100644 index 0000000..e3f2f0e --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_AudioStreamBuffered_android.cpp @@ -0,0 +1,285 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "oboe_oboe_Oboe_android.h" + +#include "oboe_common_OboeDebug_android.h" +#include "oboe_oboe_AudioClock_android.h" +#include "oboe_opensles_AudioStreamBuffered_android.h" + +namespace oboe { + +constexpr int kDefaultBurstsPerBuffer = 16; // arbitrary, allows dynamic latency tuning +constexpr int kMinBurstsPerBuffer = 4; // arbitrary, allows dynamic latency tuning +constexpr int kMinFramesPerBuffer = 48 * 32; // arbitrary + +/* + * AudioStream with a FifoBuffer + */ +AudioStreamBuffered::AudioStreamBuffered(const AudioStreamBuilder &builder) + : AudioStream(builder) { +} + +void AudioStreamBuffered::allocateFifo() { + // If the caller does not provide a callback use our own internal + // callback that reads data from the FIFO. + if (usingFIFO()) { + // FIFO is configured with the same format and channels as the stream. + int32_t capacityFrames = getBufferCapacityInFrames(); + if (capacityFrames == oboe::kUnspecified) { + capacityFrames = getFramesPerBurst() * kDefaultBurstsPerBuffer; + } else { + int32_t minFramesPerBufferByBursts = getFramesPerBurst() * kMinBurstsPerBuffer; + if (capacityFrames <= minFramesPerBufferByBursts) { + capacityFrames = minFramesPerBufferByBursts; + } else { + capacityFrames = std::max(kMinFramesPerBuffer, capacityFrames); + // round up to nearest burst + int32_t numBursts = (capacityFrames + getFramesPerBurst() - 1) + / getFramesPerBurst(); + capacityFrames = numBursts * getFramesPerBurst(); + } + } + + mFifoBuffer = std::make_unique(getBytesPerFrame(), capacityFrames); + mBufferCapacityInFrames = capacityFrames; + mBufferSizeInFrames = mBufferCapacityInFrames; + } +} + +void AudioStreamBuffered::updateFramesWritten() { + if (mFifoBuffer) { + mFramesWritten = static_cast(mFifoBuffer->getWriteCounter()); + } // or else it will get updated by processBufferCallback() +} + +void AudioStreamBuffered::updateFramesRead() { + if (mFifoBuffer) { + mFramesRead = static_cast(mFifoBuffer->getReadCounter()); + } // or else it will get updated by processBufferCallback() +} + +// This is called by the OpenSL ES callback to read or write the back end of the FIFO. +DataCallbackResult AudioStreamBuffered::onDefaultCallback(void *audioData, int numFrames) { + int32_t framesTransferred = 0; + + if (getDirection() == oboe::Direction::Output) { + // Read from the FIFO and write to audioData, clear part of buffer if not enough data. + framesTransferred = mFifoBuffer->readNow(audioData, numFrames); + } else { + // Read from audioData and write to the FIFO + framesTransferred = mFifoBuffer->write(audioData, numFrames); // There is no writeNow() + } + + if (framesTransferred < numFrames) { + LOGD("AudioStreamBuffered::%s(): xrun! framesTransferred = %d, numFrames = %d", + __func__, framesTransferred, numFrames); + // TODO If we do not allow FIFO to wrap then our timestamps will drift when there is an XRun! + incrementXRunCount(); + } + markCallbackTime(static_cast(numFrames)); // so foreground knows how long to wait. + return DataCallbackResult::Continue; +} + +void AudioStreamBuffered::markCallbackTime(int32_t numFrames) { + mLastBackgroundSize = numFrames; + mBackgroundRanAtNanoseconds = AudioClock::getNanoseconds(); +} + +int64_t AudioStreamBuffered::predictNextCallbackTime() { + if (mBackgroundRanAtNanoseconds == 0) { + return 0; + } + int64_t nanosPerBuffer = (kNanosPerSecond * mLastBackgroundSize) / getSampleRate(); + const int64_t margin = 200 * kNanosPerMicrosecond; // arbitrary delay so we wake up just after + return mBackgroundRanAtNanoseconds + nanosPerBuffer + margin; +} + +// Common code for read/write. +// @return Result::OK with frames read/written, or Result::Error* +ResultWithValue AudioStreamBuffered::transfer( + void *readBuffer, + const void *writeBuffer, + int32_t numFrames, + int64_t timeoutNanoseconds) { + // Validate arguments. + if (readBuffer != nullptr && writeBuffer != nullptr) { + LOGE("AudioStreamBuffered::%s(): both buffers are not NULL", __func__); + return ResultWithValue(Result::ErrorInternal); + } + if (getDirection() == Direction::Input && readBuffer == nullptr) { + LOGE("AudioStreamBuffered::%s(): readBuffer is NULL", __func__); + return ResultWithValue(Result::ErrorNull); + } + if (getDirection() == Direction::Output && writeBuffer == nullptr) { + LOGE("AudioStreamBuffered::%s(): writeBuffer is NULL", __func__); + return ResultWithValue(Result::ErrorNull); + } + if (numFrames < 0) { + LOGE("AudioStreamBuffered::%s(): numFrames is negative", __func__); + return ResultWithValue(Result::ErrorOutOfRange); + } else if (numFrames == 0) { + return ResultWithValue(numFrames); + } + if (timeoutNanoseconds < 0) { + LOGE("AudioStreamBuffered::%s(): timeoutNanoseconds is negative", __func__); + return ResultWithValue(Result::ErrorOutOfRange); + } + + int32_t result = 0; + uint8_t *readData = reinterpret_cast(readBuffer); + const uint8_t *writeData = reinterpret_cast(writeBuffer); + int32_t framesLeft = numFrames; + int64_t timeToQuit = 0; + bool repeat = true; + + // Calculate when to timeout. + if (timeoutNanoseconds > 0) { + timeToQuit = AudioClock::getNanoseconds() + timeoutNanoseconds; + } + + // Loop until we get the data, or we have an error, or we timeout. + do { + // read or write + if (getDirection() == Direction::Input) { + result = mFifoBuffer->read(readData, framesLeft); + if (result > 0) { + readData += mFifoBuffer->convertFramesToBytes(result); + framesLeft -= result; + } + } else { + // between zero and capacity + uint32_t fullFrames = mFifoBuffer->getFullFramesAvailable(); + // Do not write above threshold size. + int32_t emptyFrames = getBufferSizeInFrames() - static_cast(fullFrames); + int32_t framesToWrite = std::max(0, std::min(framesLeft, emptyFrames)); + result = mFifoBuffer->write(writeData, framesToWrite); + if (result > 0) { + writeData += mFifoBuffer->convertFramesToBytes(result); + framesLeft -= result; + } + } + + // If we need more data then sleep and try again. + if (framesLeft > 0 && result >= 0 && timeoutNanoseconds > 0) { + int64_t timeNow = AudioClock::getNanoseconds(); + if (timeNow >= timeToQuit) { + LOGE("AudioStreamBuffered::%s(): TIMEOUT", __func__); + repeat = false; // TIMEOUT + } else { + // Figure out how long to sleep. + int64_t sleepForNanos; + int64_t wakeTimeNanos = predictNextCallbackTime(); + if (wakeTimeNanos <= 0) { + // No estimate available. Sleep for one burst. + sleepForNanos = (getFramesPerBurst() * kNanosPerSecond) / getSampleRate(); + } else { + // Don't sleep past timeout. + if (wakeTimeNanos > timeToQuit) { + wakeTimeNanos = timeToQuit; + } + sleepForNanos = wakeTimeNanos - timeNow; + // Avoid rapid loop with no sleep. + const int64_t minSleepTime = kNanosPerMillisecond; // arbitrary + if (sleepForNanos < minSleepTime) { + sleepForNanos = minSleepTime; + } + } + + AudioClock::sleepForNanos(sleepForNanos); + } + + } else { + repeat = false; + } + } while(repeat); + + if (result < 0) { + return ResultWithValue(static_cast(result)); + } else { + int32_t framesWritten = numFrames - framesLeft; + return ResultWithValue(framesWritten); + } +} + +// Write to the FIFO so the callback can read from it. +ResultWithValue AudioStreamBuffered::write(const void *buffer, + int32_t numFrames, + int64_t timeoutNanoseconds) { + if (getState() == StreamState::Closed){ + return ResultWithValue(Result::ErrorClosed); + } + + if (getDirection() == Direction::Input) { + return ResultWithValue(Result::ErrorUnavailable); // TODO review, better error code? + } + Result result = updateServiceFrameCounter(); + if (result != Result::OK) return ResultWithValue(static_cast(result)); + return transfer(nullptr, buffer, numFrames, timeoutNanoseconds); +} + +// Read data from the FIFO that was written by the callback. +ResultWithValue AudioStreamBuffered::read(void *buffer, + int32_t numFrames, + int64_t timeoutNanoseconds) { + if (getState() == StreamState::Closed){ + return ResultWithValue(Result::ErrorClosed); + } + + if (getDirection() == Direction::Output) { + return ResultWithValue(Result::ErrorUnavailable); // TODO review, better error code? + } + Result result = updateServiceFrameCounter(); + if (result != Result::OK) return ResultWithValue(static_cast(result)); + return transfer(buffer, nullptr, numFrames, timeoutNanoseconds); +} + +// Only supported when we are not using a callback. +ResultWithValue AudioStreamBuffered::setBufferSizeInFrames(int32_t requestedFrames) +{ + if (getState() == StreamState::Closed){ + return ResultWithValue(Result::ErrorClosed); + } + + if (!mFifoBuffer) { + return ResultWithValue(Result::ErrorUnimplemented); + } + + if (requestedFrames > mFifoBuffer->getBufferCapacityInFrames()) { + requestedFrames = mFifoBuffer->getBufferCapacityInFrames(); + } else if (requestedFrames < getFramesPerBurst()) { + requestedFrames = getFramesPerBurst(); + } + mBufferSizeInFrames = requestedFrames; + return ResultWithValue(requestedFrames); +} + +int32_t AudioStreamBuffered::getBufferCapacityInFrames() const { + if (mFifoBuffer) { + return mFifoBuffer->getBufferCapacityInFrames(); + } else { + return AudioStream::getBufferCapacityInFrames(); + } +} + +bool AudioStreamBuffered::isXRunCountSupported() const { + // XRun count is only supported if we're using blocking I/O (not callbacks) + return (!isDataCallbackSpecified()); +} + +} // namespace oboe diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_AudioStreamBuffered_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_AudioStreamBuffered_android.h new file mode 100644 index 0000000..0fc9651 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_AudioStreamBuffered_android.h @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_STREAM_BUFFERED_H +#define OBOE_STREAM_BUFFERED_H + +#include +#include +#include "oboe_common_OboeDebug_android.h" +#include "oboe_oboe_AudioStream_android.h" +#include "oboe_oboe_AudioStreamCallback_android.h" +#include "oboe_oboe_FifoBuffer_android.h" + +namespace oboe { + +// A stream that contains a FIFO buffer. +// This is used to implement blocking reads and writes. +class AudioStreamBuffered : public AudioStream { +public: + + AudioStreamBuffered(); + explicit AudioStreamBuffered(const AudioStreamBuilder &builder); + + void allocateFifo(); + + + ResultWithValue write(const void *buffer, + int32_t numFrames, + int64_t timeoutNanoseconds) override; + + ResultWithValue read(void *buffer, + int32_t numFrames, + int64_t timeoutNanoseconds) override; + + ResultWithValue setBufferSizeInFrames(int32_t requestedFrames) override; + + int32_t getBufferCapacityInFrames() const override; + + ResultWithValue getXRunCount() override { + return ResultWithValue(mXRunCount); + } + + bool isXRunCountSupported() const override; + +protected: + + DataCallbackResult onDefaultCallback(void *audioData, int numFrames) override; + + // If there is no callback then we need a FIFO between the App and OpenSL ES. + bool usingFIFO() const { return !isDataCallbackSpecified(); } + + virtual Result updateServiceFrameCounter() = 0; + + void updateFramesRead() override; + void updateFramesWritten() override; + +private: + + int64_t predictNextCallbackTime(); + + void markCallbackTime(int32_t numFrames); + + // Read or write to the FIFO. + // Only pass one pointer and set the other to nullptr. + ResultWithValue transfer(void *readBuffer, + const void *writeBuffer, + int32_t numFrames, + int64_t timeoutNanoseconds); + + void incrementXRunCount() { + ++mXRunCount; + } + + std::unique_ptr mFifoBuffer{}; + + int64_t mBackgroundRanAtNanoseconds = 0; + int32_t mLastBackgroundSize = 0; + int32_t mXRunCount = 0; +}; + +} // namespace oboe + +#endif //OBOE_STREAM_BUFFERED_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_AudioStreamOpenSLES_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_AudioStreamOpenSLES_android.cpp new file mode 100644 index 0000000..baa0282 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_AudioStreamOpenSLES_android.cpp @@ -0,0 +1,523 @@ +/* Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include +#include +#include + +#include "oboe_common_OboeDebug_android.h" +#include "oboe_oboe_AudioClock_android.h" +#include "oboe_oboe_AudioStream_android.h" +#include "oboe_oboe_AudioStreamBuilder_android.h" +#include "oboe_opensles_EngineOpenSLES_android.h" +#include "oboe_opensles_AudioStreamOpenSLES_android.h" +#include "oboe_opensles_OpenSLESUtilities_android.h" + +using namespace oboe; + +AudioStreamOpenSLES::AudioStreamOpenSLES(const AudioStreamBuilder &builder) + : AudioStreamBuffered(builder) { + // OpenSL ES does not support device IDs. So overwrite value from builder. + mDeviceIds.clear(); + // OpenSL ES does not support session IDs. So overwrite value from builder. + mSessionId = SessionId::None; +} + +static constexpr int32_t kHighLatencyBufferSizeMillis = 20; // typical Android period +static constexpr SLuint32 kAudioChannelCountMax = 30; // TODO Why 30? +static constexpr SLuint32 SL_ANDROID_UNKNOWN_CHANNELMASK = 0; // Matches name used internally. + +SLuint32 AudioStreamOpenSLES::channelCountToChannelMaskDefault(int channelCount) const { + if (channelCount > kAudioChannelCountMax) { + return SL_ANDROID_UNKNOWN_CHANNELMASK; + } + + SLuint32 bitfield = (1 << channelCount) - 1; + + // Check for OS at run-time. + if(getSdkVersion() >= __ANDROID_API_N__) { + return SL_ANDROID_MAKE_INDEXED_CHANNEL_MASK(bitfield); + } + + // Indexed channels masks were added in N. + // For before N, the best we can do is use a positional channel mask. + return bitfield; +} + +static bool s_isLittleEndian() { + static uint32_t value = 1; + return (*reinterpret_cast(&value) == 1); // Does address point to LSB? +} + +SLuint32 AudioStreamOpenSLES::getDefaultByteOrder() { + return s_isLittleEndian() ? SL_BYTEORDER_LITTLEENDIAN : SL_BYTEORDER_BIGENDIAN; +} + +Result AudioStreamOpenSLES::open() { +#ifndef OBOE_SUPPRESS_LOG_SPAM + LOGI("AudioStreamOpenSLES::open() chans=%d, rate=%d", mChannelCount, mSampleRate); +#endif + + // OpenSL ES only supports I16 and Float + if (mFormat != AudioFormat::I16 && mFormat != AudioFormat::Float) { + LOGW("%s() Android's OpenSL ES implementation only supports I16 and Float. Format: %s", + __func__, oboe::convertToText(mFormat)); + return Result::ErrorInvalidFormat; + } + + SLresult result = EngineOpenSLES::getInstance().open(); + if (SL_RESULT_SUCCESS != result) { + return Result::ErrorInternal; + } + + Result oboeResult = AudioStreamBuffered::open(); + if (oboeResult != Result::OK) { + EngineOpenSLES::getInstance().close(); + return oboeResult; + } + // Convert to defaults if UNSPECIFIED + if (mSampleRate == kUnspecified) { + mSampleRate = DefaultStreamValues::SampleRate; + } + if (mChannelCount == kUnspecified) { + mChannelCount = DefaultStreamValues::ChannelCount; + } + if (mContentType == kUnspecified) { + mContentType = ContentType::Music; + } + if (static_cast(mUsage) == kUnspecified) { + mUsage = Usage::Media; + } + + mSharingMode = SharingMode::Shared; + + return Result::OK; +} + + +SLresult AudioStreamOpenSLES::finishCommonOpen(SLAndroidConfigurationItf configItf) { + // Setting privacy sensitive mode and allowed capture policy are not supported for OpenSL ES. + mPrivacySensitiveMode = PrivacySensitiveMode::Unspecified; + mAllowedCapturePolicy = AllowedCapturePolicy::Unspecified; + + // Spatialization Behavior is not supported for OpenSL ES. + mSpatializationBehavior = SpatializationBehavior::Never; + + SLresult result = registerBufferQueueCallback(); + if (SL_RESULT_SUCCESS != result) { + return result; + } + + result = updateStreamParameters(configItf); + if (SL_RESULT_SUCCESS != result) { + return result; + } + + Result oboeResult = configureBufferSizes(mSampleRate); + if (Result::OK != oboeResult) { + return (SLresult) oboeResult; + } + + allocateFifo(); + + calculateDefaultDelayBeforeCloseMillis(); + + return SL_RESULT_SUCCESS; +} + +static int32_t roundUpDivideByN(int32_t x, int32_t n) { + return (x + n - 1) / n; +} + +int32_t AudioStreamOpenSLES::calculateOptimalBufferQueueLength() { + int32_t queueLength = kBufferQueueLengthDefault; + int32_t likelyFramesPerBurst = estimateNativeFramesPerBurst(); + int32_t minCapacity = mBufferCapacityInFrames; // specified by app or zero + // The buffer capacity needs to be at least twice the size of the requested callbackSize + // so that we can have double buffering. + minCapacity = std::max(minCapacity, kDoubleBufferCount * mFramesPerCallback); + if (minCapacity > 0) { + int32_t queueLengthFromCapacity = roundUpDivideByN(minCapacity, likelyFramesPerBurst); + queueLength = std::max(queueLength, queueLengthFromCapacity); + } + queueLength = std::min(queueLength, kBufferQueueLengthMax); // clip to max + // TODO Investigate the effect of queueLength on latency for normal streams. (not low latency) + return queueLength; +} + +/** + * The best information we have is if DefaultStreamValues::FramesPerBurst + * was set by the app based on AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER. + * Without that we just have to guess. + * @return + */ +int32_t AudioStreamOpenSLES::estimateNativeFramesPerBurst() { + int32_t framesPerBurst = DefaultStreamValues::FramesPerBurst; + LOGD("AudioStreamOpenSLES:%s() DefaultStreamValues::FramesPerBurst = %d", + __func__, DefaultStreamValues::FramesPerBurst); + framesPerBurst = std::max(framesPerBurst, 16); + // Calculate the size of a fixed duration high latency buffer based on sample rate. + // Estimate sample based on default options in order of priority. + int32_t sampleRate = 48000; + sampleRate = (DefaultStreamValues::SampleRate > 0) + ? DefaultStreamValues::SampleRate : sampleRate; + sampleRate = (mSampleRate > 0) ? mSampleRate : sampleRate; + int32_t framesPerHighLatencyBuffer = + (kHighLatencyBufferSizeMillis * sampleRate) / kMillisPerSecond; + // For high latency streams, use a larger buffer size. + // Performance Mode support was added in N_MR1 (7.1) + if (getSdkVersion() >= __ANDROID_API_N_MR1__ + && mPerformanceMode != PerformanceMode::LowLatency + && framesPerBurst < framesPerHighLatencyBuffer) { + // Find a multiple of framesPerBurst >= framesPerHighLatencyBuffer. + int32_t numBursts = roundUpDivideByN(framesPerHighLatencyBuffer, framesPerBurst); + framesPerBurst *= numBursts; + LOGD("AudioStreamOpenSLES:%s() NOT low latency, numBursts = %d, mSampleRate = %d, set framesPerBurst = %d", + __func__, numBursts, mSampleRate, framesPerBurst); + } + return framesPerBurst; +} + +Result AudioStreamOpenSLES::configureBufferSizes(int32_t sampleRate) { + LOGD("AudioStreamOpenSLES:%s(%d) initial mFramesPerBurst = %d, mFramesPerCallback = %d", + __func__, mSampleRate, mFramesPerBurst, mFramesPerCallback); + mFramesPerBurst = estimateNativeFramesPerBurst(); + mFramesPerCallback = (mFramesPerCallback > 0) ? mFramesPerCallback : mFramesPerBurst; + LOGD("AudioStreamOpenSLES:%s(%d) final mFramesPerBurst = %d, mFramesPerCallback = %d", + __func__, mSampleRate, mFramesPerBurst, mFramesPerCallback); + + mBytesPerCallback = mFramesPerCallback * getBytesPerFrame(); + if (mBytesPerCallback <= 0) { + LOGE("AudioStreamOpenSLES::open() bytesPerCallback < 0 = %d, bad format?", + mBytesPerCallback); + return Result::ErrorInvalidFormat; // causing bytesPerFrame == 0 + } + + for (int i = 0; i < mBufferQueueLength; ++i) { + mCallbackBuffer[i] = std::make_unique(mBytesPerCallback); + } + + if (!usingFIFO()) { + mBufferCapacityInFrames = mFramesPerBurst * mBufferQueueLength; + // Check for overflow. + if (mBufferCapacityInFrames <= 0) { + mBufferCapacityInFrames = 0; + LOGE("AudioStreamOpenSLES::open() numeric overflow because mFramesPerBurst = %d", + mFramesPerBurst); + return Result::ErrorOutOfRange; + } + mBufferSizeInFrames = mBufferCapacityInFrames; + } + + return Result::OK; +} + +SLuint32 AudioStreamOpenSLES::convertPerformanceMode(PerformanceMode oboeMode) const { + SLuint32 openslMode = SL_ANDROID_PERFORMANCE_NONE; + switch(oboeMode) { + case PerformanceMode::None: + openslMode = SL_ANDROID_PERFORMANCE_NONE; + break; + case PerformanceMode::LowLatency: + openslMode = (getSessionId() == SessionId::None) ? SL_ANDROID_PERFORMANCE_LATENCY : SL_ANDROID_PERFORMANCE_LATENCY_EFFECTS; + break; + case PerformanceMode::PowerSaving: + openslMode = SL_ANDROID_PERFORMANCE_POWER_SAVING; + break; + default: + break; + } + return openslMode; +} + +PerformanceMode AudioStreamOpenSLES::convertPerformanceMode(SLuint32 openslMode) const { + PerformanceMode oboeMode = PerformanceMode::None; + switch(openslMode) { + case SL_ANDROID_PERFORMANCE_NONE: + oboeMode = PerformanceMode::None; + break; + case SL_ANDROID_PERFORMANCE_LATENCY: + case SL_ANDROID_PERFORMANCE_LATENCY_EFFECTS: + oboeMode = PerformanceMode::LowLatency; + break; + case SL_ANDROID_PERFORMANCE_POWER_SAVING: + oboeMode = PerformanceMode::PowerSaving; + break; + default: + break; + } + return oboeMode; +} + +void AudioStreamOpenSLES::logUnsupportedAttributes() { + // Log unsupported attributes + // only report if changed from the default + + // Device ID + if (!mDeviceIds.empty()) { + LOGW("Device ID [AudioStreamBuilder::setDeviceId()] " + "is not supported on OpenSLES streams."); + } + // Sharing Mode + if (mSharingMode != SharingMode::Shared) { + LOGW("SharingMode [AudioStreamBuilder::setSharingMode()] " + "is not supported on OpenSLES streams."); + } + // Performance Mode + int sdkVersion = getSdkVersion(); + if (mPerformanceMode != PerformanceMode::None && sdkVersion < __ANDROID_API_N_MR1__) { + LOGW("PerformanceMode [AudioStreamBuilder::setPerformanceMode()] " + "is not supported on OpenSLES streams running on pre-Android N-MR1 versions."); + } + // Content Type + if (static_cast(mContentType) != kUnspecified) { + LOGW("ContentType [AudioStreamBuilder::setContentType()] " + "is not supported on OpenSLES streams."); + } + + // Session Id + if (mSessionId != SessionId::None) { + LOGW("SessionId [AudioStreamBuilder::setSessionId()] " + "is not supported on OpenSLES streams."); + } + + // Privacy Sensitive Mode + if (mPrivacySensitiveMode != PrivacySensitiveMode::Unspecified) { + LOGW("PrivacySensitiveMode [AudioStreamBuilder::setPrivacySensitiveMode()] " + "is not supported on OpenSLES streams."); + } + + // Spatialization Behavior + if (mSpatializationBehavior != SpatializationBehavior::Unspecified) { + LOGW("SpatializationBehavior [AudioStreamBuilder::setSpatializationBehavior()] " + "is not supported on OpenSLES streams."); + } + + if (mIsContentSpatialized) { + LOGW("Boolean [AudioStreamBuilder::setIsContentSpatialized()] " + "is not supported on OpenSLES streams."); + } + + // Allowed Capture Policy + if (mAllowedCapturePolicy != AllowedCapturePolicy::Unspecified) { + LOGW("AllowedCapturePolicy [AudioStreamBuilder::setAllowedCapturePolicy()] " + "is not supported on OpenSLES streams."); + } + + // Package Name + if (!mPackageName.empty()) { + LOGW("PackageName [AudioStreamBuilder::setPackageName()] " + "is not supported on OpenSLES streams."); + } + + // Attribution Tag + if (!mAttributionTag.empty()) { + LOGW("AttributionTag [AudioStreamBuilder::setAttributionTag()] " + "is not supported on OpenSLES streams."); + } +} + +SLresult AudioStreamOpenSLES::configurePerformanceMode(SLAndroidConfigurationItf configItf) { + + if (configItf == nullptr) { + LOGW("%s() called with NULL configuration", __func__); + mPerformanceMode = PerformanceMode::None; + return SL_RESULT_INTERNAL_ERROR; + } + if (getSdkVersion() < __ANDROID_API_N_MR1__) { + LOGW("%s() not supported until N_MR1", __func__); + mPerformanceMode = PerformanceMode::None; + return SL_RESULT_SUCCESS; + } + + SLresult result = SL_RESULT_SUCCESS; + SLuint32 performanceMode = convertPerformanceMode(getPerformanceMode()); + result = (*configItf)->SetConfiguration(configItf, SL_ANDROID_KEY_PERFORMANCE_MODE, + &performanceMode, sizeof(performanceMode)); + if (SL_RESULT_SUCCESS != result) { + LOGW("SetConfiguration(PERFORMANCE_MODE, SL %u) returned %s", + performanceMode, getSLErrStr(result)); + mPerformanceMode = PerformanceMode::None; + } + + return result; +} + +SLresult AudioStreamOpenSLES::updateStreamParameters(SLAndroidConfigurationItf configItf) { + SLresult result = SL_RESULT_SUCCESS; + if(getSdkVersion() >= __ANDROID_API_N_MR1__ && configItf != nullptr) { + SLuint32 performanceMode = 0; + SLuint32 performanceModeSize = sizeof(performanceMode); + result = (*configItf)->GetConfiguration(configItf, SL_ANDROID_KEY_PERFORMANCE_MODE, + &performanceModeSize, &performanceMode); + // A bug in GetConfiguration() before P caused a wrong result code to be returned. + if (getSdkVersion() <= __ANDROID_API_O_MR1__) { + result = SL_RESULT_SUCCESS; // Ignore actual result before P. + } + + if (SL_RESULT_SUCCESS != result) { + LOGW("GetConfiguration(SL_ANDROID_KEY_PERFORMANCE_MODE) returned %d", result); + mPerformanceMode = PerformanceMode::None; // If we can't query it then assume None. + } else { + mPerformanceMode = convertPerformanceMode(performanceMode); // convert SL to Oboe mode + } + } else { + mPerformanceMode = PerformanceMode::None; // If we can't query it then assume None. + } + return result; +} + +// This is called under mLock. +Result AudioStreamOpenSLES::close_l() { + LOGD("AudioOutputStreamOpenSLES::%s() called", __func__); + if (mState == StreamState::Closed) { + return Result::ErrorClosed; + } + + AudioStreamBuffered::close(); + + onBeforeDestroy(); + + // Mark as CLOSED before we unlock for the join. + // This will prevent other threads from trying to close(). + setState(StreamState::Closed); + + SLObjectItf tempObjectInterface = mObjectInterface; + mObjectInterface = nullptr; + if (tempObjectInterface != nullptr) { + // Temporarily unlock so we can join() the callback thread. + mLock.unlock(); + (*tempObjectInterface)->Destroy(tempObjectInterface); // Will join the callback! + mLock.lock(); + } + + onAfterDestroy(); + + mSimpleBufferQueueInterface = nullptr; + EngineOpenSLES::getInstance().close(); + + return Result::OK; +} + +SLresult AudioStreamOpenSLES::enqueueCallbackBuffer(SLAndroidSimpleBufferQueueItf bq) { + SLresult result = (*bq)->Enqueue( + bq, mCallbackBuffer[mCallbackBufferIndex].get(), mBytesPerCallback); + mCallbackBufferIndex = (mCallbackBufferIndex + 1) % mBufferQueueLength; + return result; +} + +int32_t AudioStreamOpenSLES::getBufferDepth(SLAndroidSimpleBufferQueueItf bq) { + SLAndroidSimpleBufferQueueState queueState; + SLresult result = (*bq)->GetState(bq, &queueState); + return (result == SL_RESULT_SUCCESS) ? queueState.count : -1; +} + +bool AudioStreamOpenSLES::processBufferCallback(SLAndroidSimpleBufferQueueItf bq) { + bool shouldStopStream = false; + // Ask the app callback to process the buffer. + DataCallbackResult result = + fireDataCallback(mCallbackBuffer[mCallbackBufferIndex].get(), mFramesPerCallback); + if (result == DataCallbackResult::Continue) { + // Pass the buffer to OpenSLES. + SLresult enqueueResult = enqueueCallbackBuffer(bq); + if (enqueueResult != SL_RESULT_SUCCESS) { + LOGE("%s() returned %d", __func__, enqueueResult); + shouldStopStream = true; + } + // Update Oboe client position with frames handled by the callback. + if (getDirection() == Direction::Input) { + mFramesRead += mFramesPerCallback; + } else { + mFramesWritten += mFramesPerCallback; + } + } else if (result == DataCallbackResult::Stop) { + LOGD("Oboe callback returned Stop"); + shouldStopStream = true; + } else { + LOGW("Oboe callback returned unexpected value = %d", static_cast(result)); + shouldStopStream = true; + } + if (shouldStopStream) { + mCallbackBufferIndex = 0; + } + return shouldStopStream; +} + +// This callback handler is called every time a buffer has been processed by OpenSL ES. +static void bqCallbackGlue(SLAndroidSimpleBufferQueueItf bq, void *context) { + bool shouldStopStream = (reinterpret_cast(context)) + ->processBufferCallback(bq); + if (shouldStopStream) { + (reinterpret_cast(context))->requestStop(); + } +} + +SLresult AudioStreamOpenSLES::registerBufferQueueCallback() { + // The BufferQueue + SLresult result = (*mObjectInterface)->GetInterface(mObjectInterface, + EngineOpenSLES::getInstance().getIidAndroidSimpleBufferQueue(), + &mSimpleBufferQueueInterface); + if (SL_RESULT_SUCCESS != result) { + LOGE("get buffer queue interface:%p result:%s", + mSimpleBufferQueueInterface, + getSLErrStr(result)); + } else { + // Register the BufferQueue callback + result = (*mSimpleBufferQueueInterface)->RegisterCallback(mSimpleBufferQueueInterface, + bqCallbackGlue, this); + if (SL_RESULT_SUCCESS != result) { + LOGE("RegisterCallback result:%s", getSLErrStr(result)); + } + } + return result; +} + +int64_t AudioStreamOpenSLES::getFramesProcessedByServer() { + updateServiceFrameCounter(); + int64_t millis64 = mPositionMillis.get(); + int64_t framesProcessed = millis64 * getSampleRate() / kMillisPerSecond; + return framesProcessed; +} + +Result AudioStreamOpenSLES::waitForStateChange(StreamState currentState, + StreamState *nextState, + int64_t timeoutNanoseconds) { + Result oboeResult = Result::ErrorTimeout; + int64_t sleepTimeNanos = 20 * kNanosPerMillisecond; // arbitrary + int64_t timeLeftNanos = timeoutNanoseconds; + + while (true) { + const StreamState state = getState(); // this does not require a lock + if (nextState != nullptr) { + *nextState = state; + } + if (currentState != state) { // state changed? + oboeResult = Result::OK; + break; + } + + // Did we timeout or did user ask for non-blocking? + if (timeLeftNanos <= 0) { + break; + } + + if (sleepTimeNanos > timeLeftNanos){ + sleepTimeNanos = timeLeftNanos; + } + AudioClock::sleepForNanos(sleepTimeNanos); + timeLeftNanos -= sleepTimeNanos; + } + + return oboeResult; +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_AudioStreamOpenSLES_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_AudioStreamOpenSLES_android.h new file mode 100644 index 0000000..24e834a --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_AudioStreamOpenSLES_android.h @@ -0,0 +1,145 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_AUDIO_STREAM_OPENSL_ES_H_ +#define OBOE_AUDIO_STREAM_OPENSL_ES_H_ + +#include + +#include "oboe_oboe_Oboe_android.h" +#include "oboe_common_MonotonicCounter_android.h" +#include "oboe_opensles_AudioStreamBuffered_android.h" +#include "oboe_opensles_EngineOpenSLES_android.h" + +namespace oboe { + +constexpr int kBitsPerByte = 8; +constexpr int kBufferQueueLengthDefault = 2; // double buffered for callbacks +constexpr int kBufferQueueLengthMax = 8; // AudioFlinger won't use more than 8 + +/** + * INTERNAL USE ONLY + * + * A stream that wraps OpenSL ES. + * + * Do not instantiate this class directly. + * Use an OboeStreamBuilder to create one. + */ + +class AudioStreamOpenSLES : public AudioStreamBuffered { +public: + + AudioStreamOpenSLES(); + explicit AudioStreamOpenSLES(const AudioStreamBuilder &builder); + + virtual ~AudioStreamOpenSLES() = default; + + virtual Result open() override; + + /** + * Query the current state, eg. OBOE_STREAM_STATE_PAUSING + * + * @return state or a negative error. + */ + StreamState getState() override { return mState.load(); } + + AudioApi getAudioApi() const override { + return AudioApi::OpenSLES; + } + + /** + * Process next OpenSL ES buffer. + * Called by by OpenSL ES framework. + * + * This is public, but don't call it directly. + * + * @return whether the current stream should be stopped. + */ + bool processBufferCallback(SLAndroidSimpleBufferQueueItf bq); + + Result waitForStateChange(StreamState currentState, + StreamState *nextState, + int64_t timeoutNanoseconds) override; + +protected: + + /** + * Finish setting up the stream. Common for INPUT and OUTPUT. + * + * @param configItf + * @return SL_RESULT_SUCCESS if OK. + */ + SLresult finishCommonOpen(SLAndroidConfigurationItf configItf); + + // This must be called under mLock. + Result close_l(); + + SLuint32 channelCountToChannelMaskDefault(int channelCount) const; + + virtual Result onBeforeDestroy() { return Result::OK; } + virtual Result onAfterDestroy() { return Result::OK; } + + static SLuint32 getDefaultByteOrder(); + + int32_t getBufferDepth(SLAndroidSimpleBufferQueueItf bq); + + int32_t calculateOptimalBufferQueueLength(); + int32_t estimateNativeFramesPerBurst(); + + SLresult enqueueCallbackBuffer(SLAndroidSimpleBufferQueueItf bq); + + SLresult configurePerformanceMode(SLAndroidConfigurationItf configItf); + + PerformanceMode convertPerformanceMode(SLuint32 openslMode) const; + SLuint32 convertPerformanceMode(PerformanceMode oboeMode) const; + + void logUnsupportedAttributes(); + + /** + * Internal use only. + * Use this instead of directly setting the internal state variable. + */ + void setState(StreamState state) { + mState.store(state); + } + + int64_t getFramesProcessedByServer(); + + // OpenSLES stuff + SLObjectItf mObjectInterface = nullptr; + SLAndroidSimpleBufferQueueItf mSimpleBufferQueueInterface = nullptr; + int mBufferQueueLength = 0; + + int32_t mBytesPerCallback = oboe::kUnspecified; + MonotonicCounter mPositionMillis; // for tracking OpenSL ES service position + +private: + + constexpr static int kDoubleBufferCount = 2; + + SLresult registerBufferQueueCallback(); + SLresult updateStreamParameters(SLAndroidConfigurationItf configItf); + Result configureBufferSizes(int32_t sampleRate); + + std::unique_ptr mCallbackBuffer[kBufferQueueLengthMax]; + int mCallbackBufferIndex = 0; + std::atomic mState{StreamState::Uninitialized}; + +}; + +} // namespace oboe + +#endif // OBOE_AUDIO_STREAM_OPENSL_ES_H_ diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_EngineOpenSLES_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_EngineOpenSLES_android.cpp new file mode 100644 index 0000000..2671ac5 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_EngineOpenSLES_android.cpp @@ -0,0 +1,206 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "oboe_common_OboeDebug_android.h" +#include "oboe_opensles_EngineOpenSLES_android.h" +#include "oboe_opensles_OpenSLESUtilities_android.h" + +using namespace oboe; + +// OpenSL ES is deprecated in SDK 30. +// So we use custom dynamic linking to access the library. +#define LIB_OPENSLES_NAME "libOpenSLES.so" + +EngineOpenSLES &EngineOpenSLES::getInstance() { + static EngineOpenSLES sInstance; + return sInstance; +} + +// Satisfy extern in OpenSLES.h +// These are required because of b/337360630, which was causing +// Oboe to have link failures if libOpenSLES.so was not available. +// If you are statically linking Oboe and libOpenSLES.so in a shared library +// and you observe crashes, you can pass DO_NOT_DEFINE_OPENSL_ES_CONSTANTS to cmake. +#ifndef DO_NOT_DEFINE_OPENSL_ES_CONSTANTS +SL_API const SLInterfaceID SL_IID_ENGINE = nullptr; +SL_API const SLInterfaceID SL_IID_ANDROIDSIMPLEBUFFERQUEUE = nullptr; +SL_API const SLInterfaceID SL_IID_ANDROIDCONFIGURATION = nullptr; +SL_API const SLInterfaceID SL_IID_RECORD = nullptr; +SL_API const SLInterfaceID SL_IID_BUFFERQUEUE = nullptr; +SL_API const SLInterfaceID SL_IID_VOLUME = nullptr; +SL_API const SLInterfaceID SL_IID_PLAY = nullptr; +#endif + +static const char *getSafeDlerror() { + static const char *defaultMessage = "not found?"; + char *errorMessage = dlerror(); + return (errorMessage == nullptr) ? defaultMessage : errorMessage; +} + +// Load the OpenSL ES library and the one primary entry point. +// @return true if linked OK +bool EngineOpenSLES::linkOpenSLES() { + if (mDynamicLinkState == kLinkStateBad) { + LOGE("%s(), OpenSL ES not available, based on previous link failure.", __func__); + } else if (mDynamicLinkState == kLinkStateUninitialized) { + // Set to BAD now in case we return because of an error. + // This is safe form race conditions because this function is always called + // under mLock amd the state is only accessed from this function. + mDynamicLinkState = kLinkStateBad; + // Use RTLD_NOW to avoid the unpredictable behavior that RTLD_LAZY can cause. + // Also resolving all the links now will prevent a run-time penalty later. + mLibOpenSlesLibraryHandle = dlopen(LIB_OPENSLES_NAME, RTLD_NOW); + if (mLibOpenSlesLibraryHandle == nullptr) { + LOGE("%s() could not dlopen(%s), %s", __func__, LIB_OPENSLES_NAME, getSafeDlerror()); + return false; + } else { + mFunction_slCreateEngine = (prototype_slCreateEngine) dlsym( + mLibOpenSlesLibraryHandle, + "slCreateEngine"); + LOGD("%s(): dlsym(%s) returned %p", __func__, + "slCreateEngine", mFunction_slCreateEngine); + if (mFunction_slCreateEngine == nullptr) { + LOGE("%s(): dlsym(slCreateEngine) returned null, %s", __func__, getSafeDlerror()); + return false; + } + + // Load IID interfaces. + LOCAL_SL_IID_ENGINE = getIidPointer("SL_IID_ENGINE"); + if (LOCAL_SL_IID_ENGINE == nullptr) return false; + LOCAL_SL_IID_ANDROIDSIMPLEBUFFERQUEUE = getIidPointer( + "SL_IID_ANDROIDSIMPLEBUFFERQUEUE"); + if (LOCAL_SL_IID_ANDROIDSIMPLEBUFFERQUEUE == nullptr) return false; + LOCAL_SL_IID_ANDROIDCONFIGURATION = getIidPointer( + "SL_IID_ANDROIDCONFIGURATION"); + if (LOCAL_SL_IID_ANDROIDCONFIGURATION == nullptr) return false; + LOCAL_SL_IID_RECORD = getIidPointer("SL_IID_RECORD"); + if (LOCAL_SL_IID_RECORD == nullptr) return false; + LOCAL_SL_IID_BUFFERQUEUE = getIidPointer("SL_IID_BUFFERQUEUE"); + if (LOCAL_SL_IID_BUFFERQUEUE == nullptr) return false; + LOCAL_SL_IID_VOLUME = getIidPointer("SL_IID_VOLUME"); + if (LOCAL_SL_IID_VOLUME == nullptr) return false; + LOCAL_SL_IID_PLAY = getIidPointer("SL_IID_PLAY"); + if (LOCAL_SL_IID_PLAY == nullptr) return false; + + mDynamicLinkState = kLinkStateGood; + } + } + return (mDynamicLinkState == kLinkStateGood); +} + +// A symbol like SL_IID_PLAY is a pointer to a structure. +// The dlsym() function returns the address of the pointer, not the structure. +// To get the address of the structure we have to dereference the pointer. +SLInterfaceID EngineOpenSLES::getIidPointer(const char *symbolName) { + SLInterfaceID *iid_address = (SLInterfaceID *) dlsym( + mLibOpenSlesLibraryHandle, + symbolName); + if (iid_address == nullptr) { + LOGE("%s(): dlsym(%s) returned null, %s", __func__, symbolName, getSafeDlerror()); + return (SLInterfaceID) nullptr; + } + return *iid_address; // Get address of the structure. +} + +SLresult EngineOpenSLES::open() { + std::lock_guard lock(mLock); + + SLresult result = SL_RESULT_SUCCESS; + if (mOpenCount++ == 0) { + // load the library and link to it + if (!linkOpenSLES()) { + result = SL_RESULT_FEATURE_UNSUPPORTED; + goto error; + }; + + // create engine + result = (*mFunction_slCreateEngine)(&mEngineObject, 0, NULL, 0, NULL, NULL); + if (SL_RESULT_SUCCESS != result) { + LOGE("EngineOpenSLES - slCreateEngine() result:%s", getSLErrStr(result)); + goto error; + } + + // realize the engine + result = (*mEngineObject)->Realize(mEngineObject, SL_BOOLEAN_FALSE); + if (SL_RESULT_SUCCESS != result) { + LOGE("EngineOpenSLES - Realize() engine result:%s", getSLErrStr(result)); + goto error; + } + + // get the engine interface, which is needed in order to create other objects + result = (*mEngineObject)->GetInterface(mEngineObject, + EngineOpenSLES::getInstance().getIidEngine(), + &mEngineInterface); + if (SL_RESULT_SUCCESS != result) { + LOGE("EngineOpenSLES - GetInterface() engine result:%s", getSLErrStr(result)); + goto error; + } + } + + return result; + +error: + close_l(); + return result; +} + +void EngineOpenSLES::close() { + std::lock_guard lock(mLock); + close_l(); +} + +// This must be called under mLock +void EngineOpenSLES::close_l() { + if (--mOpenCount == 0) { + if (mEngineObject != nullptr) { + (*mEngineObject)->Destroy(mEngineObject); + mEngineObject = nullptr; + mEngineInterface = nullptr; + } + } +} + +SLresult EngineOpenSLES::createOutputMix(SLObjectItf *objectItf) { + return (*mEngineInterface)->CreateOutputMix(mEngineInterface, objectItf, 0, 0, 0); +} + +SLresult EngineOpenSLES::createAudioPlayer(SLObjectItf *objectItf, + SLDataSource *audioSource, + SLDataSink *audioSink) { + + SLInterfaceID ids[] = {LOCAL_SL_IID_BUFFERQUEUE, LOCAL_SL_IID_ANDROIDCONFIGURATION}; + SLboolean reqs[] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE}; + + return (*mEngineInterface)->CreateAudioPlayer(mEngineInterface, objectItf, audioSource, + audioSink, + sizeof(ids) / sizeof(ids[0]), ids, reqs); +} + +SLresult EngineOpenSLES::createAudioRecorder(SLObjectItf *objectItf, + SLDataSource *audioSource, + SLDataSink *audioSink) { + + SLInterfaceID ids[] = {LOCAL_SL_IID_ANDROIDSIMPLEBUFFERQUEUE, + LOCAL_SL_IID_ANDROIDCONFIGURATION }; + SLboolean reqs[] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE}; + + return (*mEngineInterface)->CreateAudioRecorder(mEngineInterface, objectItf, audioSource, + audioSink, + sizeof(ids) / sizeof(ids[0]), ids, reqs); +} + diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_EngineOpenSLES_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_EngineOpenSLES_android.h new file mode 100644 index 0000000..f856406 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_EngineOpenSLES_android.h @@ -0,0 +1,107 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_ENGINE_OPENSLES_H +#define OBOE_ENGINE_OPENSLES_H + +#include +#include + +#include +#include + +namespace oboe { + +typedef SLresult (*prototype_slCreateEngine)( + SLObjectItf *pEngine, + SLuint32 numOptions, + const SLEngineOption *pEngineOptions, + SLuint32 numInterfaces, + const SLInterfaceID *pInterfaceIds, + const SLboolean *pInterfaceRequired +); + +/** + * INTERNAL USE ONLY + */ +class EngineOpenSLES { +public: + static EngineOpenSLES &getInstance(); + + bool linkOpenSLES(); + + SLresult open(); + + void close(); + + SLresult createOutputMix(SLObjectItf *objectItf); + + SLresult createAudioPlayer(SLObjectItf *objectItf, + SLDataSource *audioSource, + SLDataSink *audioSink); + SLresult createAudioRecorder(SLObjectItf *objectItf, + SLDataSource *audioSource, + SLDataSink *audioSink); + + SLInterfaceID getIidEngine() { return LOCAL_SL_IID_ENGINE; } + SLInterfaceID getIidAndroidSimpleBufferQueue() { return LOCAL_SL_IID_ANDROIDSIMPLEBUFFERQUEUE; } + SLInterfaceID getIidAndroidConfiguration() { return LOCAL_SL_IID_ANDROIDCONFIGURATION; } + SLInterfaceID getIidRecord() { return LOCAL_SL_IID_RECORD; } + SLInterfaceID getIidBufferQueue() { return LOCAL_SL_IID_BUFFERQUEUE; } + SLInterfaceID getIidVolume() { return LOCAL_SL_IID_VOLUME; } + SLInterfaceID getIidPlay() { return LOCAL_SL_IID_PLAY; } + +private: + // Make this a safe Singleton + EngineOpenSLES()= default; + ~EngineOpenSLES()= default; + EngineOpenSLES(const EngineOpenSLES&)= delete; + EngineOpenSLES& operator=(const EngineOpenSLES&)= delete; + + SLInterfaceID getIidPointer(const char *symbolName); + + /** + * Close the OpenSL ES engine. + * This must be called under mLock + */ + void close_l(); + + std::mutex mLock; + int32_t mOpenCount = 0; + + static constexpr int32_t kLinkStateUninitialized = 0; + static constexpr int32_t kLinkStateGood = 1; + static constexpr int32_t kLinkStateBad = 2; + int32_t mDynamicLinkState = kLinkStateUninitialized; + SLObjectItf mEngineObject = nullptr; + SLEngineItf mEngineInterface = nullptr; + + // These symbols are loaded using dlsym(). + prototype_slCreateEngine mFunction_slCreateEngine = nullptr; + void *mLibOpenSlesLibraryHandle = nullptr; + SLInterfaceID LOCAL_SL_IID_ENGINE = nullptr; + SLInterfaceID LOCAL_SL_IID_ANDROIDSIMPLEBUFFERQUEUE = nullptr; + SLInterfaceID LOCAL_SL_IID_ANDROIDCONFIGURATION = nullptr; + SLInterfaceID LOCAL_SL_IID_RECORD = nullptr; + SLInterfaceID LOCAL_SL_IID_BUFFERQUEUE = nullptr; + SLInterfaceID LOCAL_SL_IID_VOLUME = nullptr; + SLInterfaceID LOCAL_SL_IID_PLAY = nullptr; +}; + +} // namespace oboe + + +#endif //OBOE_ENGINE_OPENSLES_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_OpenSLESUtilities_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_OpenSLESUtilities_android.cpp new file mode 100644 index 0000000..2e4dc29 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_OpenSLESUtilities_android.cpp @@ -0,0 +1,103 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "oboe_opensles_OpenSLESUtilities_android.h" + +namespace oboe { + +/* + * OSLES Helpers + */ + +const char *getSLErrStr(SLresult code) { + switch (code) { + case SL_RESULT_SUCCESS: + return "SL_RESULT_SUCCESS"; + case SL_RESULT_PRECONDITIONS_VIOLATED: + return "SL_RESULT_PRECONDITIONS_VIOLATED"; + case SL_RESULT_PARAMETER_INVALID: + return "SL_RESULT_PARAMETER_INVALID"; + case SL_RESULT_MEMORY_FAILURE: + return "SL_RESULT_MEMORY_FAILURE"; + case SL_RESULT_RESOURCE_ERROR: + return "SL_RESULT_RESOURCE_ERROR"; + case SL_RESULT_RESOURCE_LOST: + return "SL_RESULT_RESOURCE_LOST"; + case SL_RESULT_IO_ERROR: + return "SL_RESULT_IO_ERROR"; + case SL_RESULT_BUFFER_INSUFFICIENT: + return "SL_RESULT_BUFFER_INSUFFICIENT"; + case SL_RESULT_CONTENT_CORRUPTED: + return "SL_RESULT_CONTENT_CORRUPTED"; + case SL_RESULT_CONTENT_UNSUPPORTED: + return "SL_RESULT_CONTENT_UNSUPPORTED"; + case SL_RESULT_CONTENT_NOT_FOUND: + return "SL_RESULT_CONTENT_NOT_FOUND"; + case SL_RESULT_PERMISSION_DENIED: + return "SL_RESULT_PERMISSION_DENIED"; + case SL_RESULT_FEATURE_UNSUPPORTED: + return "SL_RESULT_FEATURE_UNSUPPORTED"; + case SL_RESULT_INTERNAL_ERROR: + return "SL_RESULT_INTERNAL_ERROR"; + case SL_RESULT_UNKNOWN_ERROR: + return "SL_RESULT_UNKNOWN_ERROR"; + case SL_RESULT_OPERATION_ABORTED: + return "SL_RESULT_OPERATION_ABORTED"; + case SL_RESULT_CONTROL_LOST: + return "SL_RESULT_CONTROL_LOST"; + default: + return "Unknown SL error"; + } +} + +SLAndroidDataFormat_PCM_EX OpenSLES_createExtendedFormat( + SLDataFormat_PCM format, SLuint32 representation) { + SLAndroidDataFormat_PCM_EX format_pcm_ex; + format_pcm_ex.formatType = SL_ANDROID_DATAFORMAT_PCM_EX; + format_pcm_ex.numChannels = format.numChannels; + format_pcm_ex.sampleRate = format.samplesPerSec; + format_pcm_ex.bitsPerSample = format.bitsPerSample; + format_pcm_ex.containerSize = format.containerSize; + format_pcm_ex.channelMask = format.channelMask; + format_pcm_ex.endianness = format.endianness; + format_pcm_ex.representation = representation; + return format_pcm_ex; +} + +SLuint32 OpenSLES_ConvertFormatToRepresentation(AudioFormat format) { + switch(format) { + case AudioFormat::I16: + return SL_ANDROID_PCM_REPRESENTATION_SIGNED_INT; + case AudioFormat::Float: + return SL_ANDROID_PCM_REPRESENTATION_FLOAT; + case AudioFormat::I24: + case AudioFormat::I32: + case AudioFormat::IEC61937: + case AudioFormat::Invalid: + case AudioFormat::Unspecified: + case AudioFormat::MP3: + case AudioFormat::AAC_LC: + case AudioFormat::AAC_HE_V1: + case AudioFormat::AAC_HE_V2: + case AudioFormat::AAC_ELD: + case AudioFormat::AAC_XHE: + case AudioFormat::OPUS: + default: + return 0; + } +} + +} // namespace oboe diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_OpenSLESUtilities_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_OpenSLESUtilities_android.h new file mode 100644 index 0000000..7736d35 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_OpenSLESUtilities_android.h @@ -0,0 +1,44 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_OPENSLES_OPENSLESUTILITIES_H +#define OBOE_OPENSLES_OPENSLESUTILITIES_H + +#include +#include "oboe_oboe_Oboe_android.h" + +namespace oboe { + +const char *getSLErrStr(SLresult code); + +/** + * Creates an extended PCM format from the supplied format and data representation. This method + * should only be called on Android devices with API level 21+. API 21 introduced the + * SLAndroidDataFormat_PCM_EX object which allows audio samples to be represented using + * single precision floating-point. + * + * @param format + * @param representation + * @return the extended PCM format + */ +SLAndroidDataFormat_PCM_EX OpenSLES_createExtendedFormat(SLDataFormat_PCM format, + SLuint32 representation); + +SLuint32 OpenSLES_ConvertFormatToRepresentation(AudioFormat format); + +} // namespace oboe + +#endif //OBOE_OPENSLES_OPENSLESUTILITIES_H diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_OutputMixerOpenSLES_android.cpp b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_OutputMixerOpenSLES_android.cpp new file mode 100644 index 0000000..b4c9e62 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_OutputMixerOpenSLES_android.cpp @@ -0,0 +1,74 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +#include "oboe_common_OboeDebug_android.h" +#include "oboe_opensles_EngineOpenSLES_android.h" +#include "oboe_opensles_OpenSLESUtilities_android.h" +#include "oboe_opensles_OutputMixerOpenSLES_android.h" + +using namespace oboe; + +OutputMixerOpenSL &OutputMixerOpenSL::getInstance() { + static OutputMixerOpenSL sInstance; + return sInstance; +} + +SLresult OutputMixerOpenSL::open() { + std::lock_guard lock(mLock); + + SLresult result = SL_RESULT_SUCCESS; + if (mOpenCount++ == 0) { + // get the output mixer + result = EngineOpenSLES::getInstance().createOutputMix(&mOutputMixObject); + if (SL_RESULT_SUCCESS != result) { + LOGE("OutputMixerOpenSL() - createOutputMix() result:%s", getSLErrStr(result)); + goto error; + } + + // realize the output mix + result = (*mOutputMixObject)->Realize(mOutputMixObject, SL_BOOLEAN_FALSE); + if (SL_RESULT_SUCCESS != result) { + LOGE("OutputMixerOpenSL() - Realize() mOutputMixObject result:%s", getSLErrStr(result)); + goto error; + } + } + + return result; + +error: + close(); + return result; +} + +void OutputMixerOpenSL::close() { + std::lock_guard lock(mLock); + + if (--mOpenCount == 0) { + // destroy output mix object, and invalidate all associated interfaces + if (mOutputMixObject != nullptr) { + (*mOutputMixObject)->Destroy(mOutputMixObject); + mOutputMixObject = nullptr; + } + } +} + +SLresult OutputMixerOpenSL::createAudioPlayer(SLObjectItf *objectItf, + SLDataSource *audioSource) { + SLDataLocator_OutputMix loc_outmix = {SL_DATALOCATOR_OUTPUTMIX, mOutputMixObject}; + SLDataSink audioSink = {&loc_outmix, NULL}; + return EngineOpenSLES::getInstance().createAudioPlayer(objectItf, audioSource, &audioSink); +} diff --git a/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_OutputMixerOpenSLES_android.h b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_OutputMixerOpenSLES_android.h new file mode 100644 index 0000000..409ee6a --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/internal/oboe/oboe_opensles_OutputMixerOpenSLES_android.h @@ -0,0 +1,57 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_OUTPUT_MIXER_OPENSLES_H +#define OBOE_OUTPUT_MIXER_OPENSLES_H + +#include +#include + +#include "oboe_opensles_EngineOpenSLES_android.h" + +namespace oboe { + +/** + * INTERNAL USE ONLY + */ + +class OutputMixerOpenSL { +public: + static OutputMixerOpenSL &getInstance(); + + SLresult open(); + + void close(); + + SLresult createAudioPlayer(SLObjectItf *objectItf, + SLDataSource *audioSource); + +private: + // Make this a safe Singleton + OutputMixerOpenSL()= default; + ~OutputMixerOpenSL()= default; + OutputMixerOpenSL(const OutputMixerOpenSL&)= delete; + OutputMixerOpenSL& operator=(const OutputMixerOpenSL&)= delete; + + std::mutex mLock; + int32_t mOpenCount = 0; + + SLObjectItf mOutputMixObject = nullptr; +}; + +} // namespace oboe + +#endif //OBOE_OUTPUT_MIXER_OPENSLES_H diff --git a/vendor/github.com/ebitengine/oto/v3/player.go b/vendor/github.com/ebitengine/oto/v3/player.go new file mode 100644 index 0000000..3015f36 --- /dev/null +++ b/vendor/github.com/ebitengine/oto/v3/player.go @@ -0,0 +1,95 @@ +// Copyright 2021 The Oto Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package oto + +import ( + "fmt" + + "github.com/ebitengine/oto/v3/internal/mux" +) + +// Player is a PCM (pulse-code modulation) audio player. +type Player struct { + player *mux.Player +} + +// Pause pauses its playing. +func (p *Player) Pause() { + p.player.Pause() +} + +// Play starts its playing if it doesn't play. +func (p *Player) Play() { + p.player.Play() +} + +// IsPlaying reports whether this player is playing. +func (p *Player) IsPlaying() bool { + return p.player.IsPlaying() +} + +// Reset clears the underyling buffer and pauses its playing. +// +// Deprecated: use Pause or Seek instead. +func (p *Player) Reset() { + p.player.Reset() +} + +// Volume returns the current volume in the range of [0, 1]. +// The default volume is 1. +func (p *Player) Volume() float64 { + return p.player.Volume() +} + +// SetVolume sets the current volume in the range of [0, 1]. +func (p *Player) SetVolume(volume float64) { + p.player.SetVolume(volume) +} + +// BufferedSize returns the byte size of the buffer data that is not sent to the audio hardware yet. +func (p *Player) BufferedSize() int { + return p.player.BufferedSize() +} + +// Err returns an error if this player has an error. +func (p *Player) Err() error { + if err := p.player.Err(); err != nil { + return fmt.Errorf("oto: audio error: %w", err) + } + return nil +} + +// SetBufferSize sets the buffer size. +// If 0 is specified, the default buffer size is used. +func (p *Player) SetBufferSize(bufferSize int) { + p.player.SetBufferSize(bufferSize) +} + +// Seek implements io.Seeker. +// +// Seek returns an error when the underlying source doesn't implement io.Seeker. +func (p *Player) Seek(offset int64, whence int) (int64, error) { + return p.player.Seek(offset, whence) +} + +// Close implements io.Closer. +// +// Close does nothing and always returns nil. +// +// Deprecated: as of v3.4. you don't have to call Close. +func (p *Player) Close() error { + // (*mux.Player).Close() is called by the finalizer. Let's rely on it. + return nil +} diff --git a/vendor/github.com/ebitengine/purego/.gitignore b/vendor/github.com/ebitengine/purego/.gitignore new file mode 100644 index 0000000..b25c15b --- /dev/null +++ b/vendor/github.com/ebitengine/purego/.gitignore @@ -0,0 +1 @@ +*~ diff --git a/vendor/github.com/ebitengine/purego/LICENSE b/vendor/github.com/ebitengine/purego/LICENSE new file mode 100644 index 0000000..8dada3e --- /dev/null +++ b/vendor/github.com/ebitengine/purego/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/ebitengine/purego/README.md b/vendor/github.com/ebitengine/purego/README.md new file mode 100644 index 0000000..523e911 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/README.md @@ -0,0 +1,113 @@ +# purego +[![Go Reference](https://pkg.go.dev/badge/github.com/ebitengine/purego?GOOS=darwin.svg)](https://pkg.go.dev/github.com/ebitengine/purego?GOOS=darwin) + +A library for calling C functions from Go without Cgo. + +> This is beta software so expect bugs and potentially API breaking changes +> but each release will be tagged to avoid breaking people's code. +> Bug reports are encouraged. + +## Motivation + +The [Ebitengine](https://github.com/hajimehoshi/ebiten) game engine was ported to use only Go on Windows. This enabled +cross-compiling to Windows from any other operating system simply by setting `GOOS=windows`. The purego project was +born to bring that same vision to the other platforms supported by Ebitengine. + +## Benefits + +- **Simple Cross-Compilation**: No C means you can build for other platforms easily without a C compiler. +- **Faster Compilation**: Efficiently cache your entirely Go builds. +- **Smaller Binaries**: Using Cgo generates a C wrapper function for each C function called. Purego doesn't! +- **Dynamic Linking**: Load symbols at runtime and use it as a plugin system. +- **Foreign Function Interface**: Call into other languages that are compiled into shared objects. +- **Cgo Fallback**: Works even with CGO_ENABLED=1 so incremental porting is possible. +This also means unsupported GOARCHs (freebsd/riscv64, linux/mips, etc.) will still work +except for float arguments and return values. + +## Supported Platforms + +### Tier 1 + +Tier 1 platforms are the primary targets officially supported by PureGo. When a new version of PureGo is released, any critical bugs found on Tier 1 platforms are treated as release blockers. The release will be postponed until such issues are resolved. + +- **Android**: amd64, arm64 +- **iOS**: amd64, arm64 +- **Linux**: amd64, arm64 +- **macOS**: amd64, arm64 +- **Windows**: amd64, arm64 + +### Tier 2 + +Tier 2 platforms are supported by PureGo on a best-effort basis. Critical bugs on Tier 2 platforms do not block new PureGo releases. However, fixes contributed by external contributors are very welcome and encouraged. + +- **Android**: 386, arm +- **FreeBSD**: amd64, arm64 +- **Linux**: 386, arm, loong64 +- **Windows**: 386*, arm* + +`*` These architectures only support `SyscallN` and `NewCallback` + +## Example + +The example below only showcases purego use for macOS and Linux. The other platforms require special handling which can +be seen in the complete example at [examples/libc](https://github.com/ebitengine/purego/tree/main/examples/libc) which supports FreeBSD and Windows. + +```go +package main + +import ( + "fmt" + "runtime" + + "github.com/ebitengine/purego" +) + +func getSystemLibrary() string { + switch runtime.GOOS { + case "darwin": + return "/usr/lib/libSystem.B.dylib" + case "linux": + return "libc.so.6" + default: + panic(fmt.Errorf("GOOS=%s is not supported", runtime.GOOS)) + } +} + +func main() { + libc, err := purego.Dlopen(getSystemLibrary(), purego.RTLD_NOW|purego.RTLD_GLOBAL) + if err != nil { + panic(err) + } + var puts func(string) + purego.RegisterLibFunc(&puts, libc, "puts") + puts("Calling C from Go without Cgo!") +} +``` + +Then to run: `CGO_ENABLED=0 go run main.go` + +## Questions + +If you have questions about how to incorporate purego in your project or want to discuss +how it works join the [Discord](https://discord.gg/HzGZVD6BkY)! + +### External Code + +Purego uses code that originates from the Go runtime. These files are under the BSD-3 +License that can be found [in the Go Source](https://github.com/golang/go/blob/master/LICENSE). +This is a list of the copied files: + +* `abi_*.h` from package `runtime/cgo` +* `wincallback.go` from package `runtime` +* `zcallback_darwin_*.s` from package `runtime` +* `internal/fakecgo/abi_*.h` from package `runtime/cgo` +* `internal/fakecgo/asm_GOARCH.s` from package `runtime/cgo` +* `internal/fakecgo/callbacks.go` from package `runtime/cgo` +* `internal/fakecgo/go_GOOS_GOARCH.go` from package `runtime/cgo` +* `internal/fakecgo/iscgo.go` from package `runtime/cgo` +* `internal/fakecgo/setenv.go` from package `runtime/cgo` +* `internal/fakecgo/freebsd.go` from package `runtime/cgo` +* `internal/fakecgo/netbsd.go` from package `runtime/cgo` + +The files `abi_*.h` and `internal/fakecgo/abi_*.h` are the same because Bazel does not support cross-package use of +`#include` so we need each one once per package. (cf. [issue](https://github.com/bazelbuild/rules_go/issues/3636)) diff --git a/vendor/github.com/ebitengine/purego/abi_amd64.h b/vendor/github.com/ebitengine/purego/abi_amd64.h new file mode 100644 index 0000000..9949435 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/abi_amd64.h @@ -0,0 +1,99 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Macros for transitioning from the host ABI to Go ABI0. +// +// These save the frame pointer, so in general, functions that use +// these should have zero frame size to suppress the automatic frame +// pointer, though it's harmless to not do this. + +#ifdef GOOS_windows + +// REGS_HOST_TO_ABI0_STACK is the stack bytes used by +// PUSH_REGS_HOST_TO_ABI0. +#define REGS_HOST_TO_ABI0_STACK (28*8 + 8) + +// PUSH_REGS_HOST_TO_ABI0 prepares for transitioning from +// the host ABI to Go ABI0 code. It saves all registers that are +// callee-save in the host ABI and caller-save in Go ABI0 and prepares +// for entry to Go. +// +// Save DI SI BP BX R12 R13 R14 R15 X6-X15 registers and the DF flag. +// Clear the DF flag for the Go ABI. +// MXCSR matches the Go ABI, so we don't have to set that, +// and Go doesn't modify it, so we don't have to save it. +#define PUSH_REGS_HOST_TO_ABI0() \ + PUSHFQ \ + CLD \ + ADJSP $(REGS_HOST_TO_ABI0_STACK - 8) \ + MOVQ DI, (0*0)(SP) \ + MOVQ SI, (1*8)(SP) \ + MOVQ BP, (2*8)(SP) \ + MOVQ BX, (3*8)(SP) \ + MOVQ R12, (4*8)(SP) \ + MOVQ R13, (5*8)(SP) \ + MOVQ R14, (6*8)(SP) \ + MOVQ R15, (7*8)(SP) \ + MOVUPS X6, (8*8)(SP) \ + MOVUPS X7, (10*8)(SP) \ + MOVUPS X8, (12*8)(SP) \ + MOVUPS X9, (14*8)(SP) \ + MOVUPS X10, (16*8)(SP) \ + MOVUPS X11, (18*8)(SP) \ + MOVUPS X12, (20*8)(SP) \ + MOVUPS X13, (22*8)(SP) \ + MOVUPS X14, (24*8)(SP) \ + MOVUPS X15, (26*8)(SP) + +#define POP_REGS_HOST_TO_ABI0() \ + MOVQ (0*0)(SP), DI \ + MOVQ (1*8)(SP), SI \ + MOVQ (2*8)(SP), BP \ + MOVQ (3*8)(SP), BX \ + MOVQ (4*8)(SP), R12 \ + MOVQ (5*8)(SP), R13 \ + MOVQ (6*8)(SP), R14 \ + MOVQ (7*8)(SP), R15 \ + MOVUPS (8*8)(SP), X6 \ + MOVUPS (10*8)(SP), X7 \ + MOVUPS (12*8)(SP), X8 \ + MOVUPS (14*8)(SP), X9 \ + MOVUPS (16*8)(SP), X10 \ + MOVUPS (18*8)(SP), X11 \ + MOVUPS (20*8)(SP), X12 \ + MOVUPS (22*8)(SP), X13 \ + MOVUPS (24*8)(SP), X14 \ + MOVUPS (26*8)(SP), X15 \ + ADJSP $-(REGS_HOST_TO_ABI0_STACK - 8) \ + POPFQ + +#else +// SysV ABI + +#define REGS_HOST_TO_ABI0_STACK (6*8) + +// SysV MXCSR matches the Go ABI, so we don't have to set that, +// and Go doesn't modify it, so we don't have to save it. +// Both SysV and Go require DF to be cleared, so that's already clear. +// The SysV and Go frame pointer conventions are compatible. +#define PUSH_REGS_HOST_TO_ABI0() \ + ADJSP $(REGS_HOST_TO_ABI0_STACK) \ + MOVQ BP, (5*8)(SP) \ + LEAQ (5*8)(SP), BP \ + MOVQ BX, (0*8)(SP) \ + MOVQ R12, (1*8)(SP) \ + MOVQ R13, (2*8)(SP) \ + MOVQ R14, (3*8)(SP) \ + MOVQ R15, (4*8)(SP) + +#define POP_REGS_HOST_TO_ABI0() \ + MOVQ (0*8)(SP), BX \ + MOVQ (1*8)(SP), R12 \ + MOVQ (2*8)(SP), R13 \ + MOVQ (3*8)(SP), R14 \ + MOVQ (4*8)(SP), R15 \ + MOVQ (5*8)(SP), BP \ + ADJSP $-(REGS_HOST_TO_ABI0_STACK) + +#endif diff --git a/vendor/github.com/ebitengine/purego/abi_arm64.h b/vendor/github.com/ebitengine/purego/abi_arm64.h new file mode 100644 index 0000000..5d5061e --- /dev/null +++ b/vendor/github.com/ebitengine/purego/abi_arm64.h @@ -0,0 +1,39 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Macros for transitioning from the host ABI to Go ABI0. +// +// These macros save and restore the callee-saved registers +// from the stack, but they don't adjust stack pointer, so +// the user should prepare stack space in advance. +// SAVE_R19_TO_R28(offset) saves R19 ~ R28 to the stack space +// of ((offset)+0*8)(RSP) ~ ((offset)+9*8)(RSP). +// +// SAVE_F8_TO_F15(offset) saves F8 ~ F15 to the stack space +// of ((offset)+0*8)(RSP) ~ ((offset)+7*8)(RSP). +// +// R29 is not saved because Go will save and restore it. + +#define SAVE_R19_TO_R28(offset) \ + STP (R19, R20), ((offset)+0*8)(RSP) \ + STP (R21, R22), ((offset)+2*8)(RSP) \ + STP (R23, R24), ((offset)+4*8)(RSP) \ + STP (R25, R26), ((offset)+6*8)(RSP) \ + STP (R27, g), ((offset)+8*8)(RSP) +#define RESTORE_R19_TO_R28(offset) \ + LDP ((offset)+0*8)(RSP), (R19, R20) \ + LDP ((offset)+2*8)(RSP), (R21, R22) \ + LDP ((offset)+4*8)(RSP), (R23, R24) \ + LDP ((offset)+6*8)(RSP), (R25, R26) \ + LDP ((offset)+8*8)(RSP), (R27, g) /* R28 */ +#define SAVE_F8_TO_F15(offset) \ + FSTPD (F8, F9), ((offset)+0*8)(RSP) \ + FSTPD (F10, F11), ((offset)+2*8)(RSP) \ + FSTPD (F12, F13), ((offset)+4*8)(RSP) \ + FSTPD (F14, F15), ((offset)+6*8)(RSP) +#define RESTORE_F8_TO_F15(offset) \ + FLDPD ((offset)+0*8)(RSP), (F8, F9) \ + FLDPD ((offset)+2*8)(RSP), (F10, F11) \ + FLDPD ((offset)+4*8)(RSP), (F12, F13) \ + FLDPD ((offset)+6*8)(RSP), (F14, F15) diff --git a/vendor/github.com/ebitengine/purego/abi_loong64.h b/vendor/github.com/ebitengine/purego/abi_loong64.h new file mode 100644 index 0000000..b10d837 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/abi_loong64.h @@ -0,0 +1,60 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Macros for transitioning from the host ABI to Go ABI0. +// +// These macros save and restore the callee-saved registers +// from the stack, but they don't adjust stack pointer, so +// the user should prepare stack space in advance. +// SAVE_R22_TO_R31(offset) saves R22 ~ R31 to the stack space +// of ((offset)+0*8)(R3) ~ ((offset)+9*8)(R3). +// +// SAVE_F24_TO_F31(offset) saves F24 ~ F31 to the stack space +// of ((offset)+0*8)(R3) ~ ((offset)+7*8)(R3). +// +// Note: g is R22 + +#define SAVE_R22_TO_R31(offset) \ + MOVV g, ((offset)+(0*8))(R3) \ + MOVV R23, ((offset)+(1*8))(R3) \ + MOVV R24, ((offset)+(2*8))(R3) \ + MOVV R25, ((offset)+(3*8))(R3) \ + MOVV R26, ((offset)+(4*8))(R3) \ + MOVV R27, ((offset)+(5*8))(R3) \ + MOVV R28, ((offset)+(6*8))(R3) \ + MOVV R29, ((offset)+(7*8))(R3) \ + MOVV R30, ((offset)+(8*8))(R3) \ + MOVV R31, ((offset)+(9*8))(R3) + +#define SAVE_F24_TO_F31(offset) \ + MOVD F24, ((offset)+(0*8))(R3) \ + MOVD F25, ((offset)+(1*8))(R3) \ + MOVD F26, ((offset)+(2*8))(R3) \ + MOVD F27, ((offset)+(3*8))(R3) \ + MOVD F28, ((offset)+(4*8))(R3) \ + MOVD F29, ((offset)+(5*8))(R3) \ + MOVD F30, ((offset)+(6*8))(R3) \ + MOVD F31, ((offset)+(7*8))(R3) + +#define RESTORE_R22_TO_R31(offset) \ + MOVV ((offset)+(0*8))(R3), g \ + MOVV ((offset)+(1*8))(R3), R23 \ + MOVV ((offset)+(2*8))(R3), R24 \ + MOVV ((offset)+(3*8))(R3), R25 \ + MOVV ((offset)+(4*8))(R3), R26 \ + MOVV ((offset)+(5*8))(R3), R27 \ + MOVV ((offset)+(6*8))(R3), R28 \ + MOVV ((offset)+(7*8))(R3), R29 \ + MOVV ((offset)+(8*8))(R3), R30 \ + MOVV ((offset)+(9*8))(R3), R31 + +#define RESTORE_F24_TO_F31(offset) \ + MOVD ((offset)+(0*8))(R3), F24 \ + MOVD ((offset)+(1*8))(R3), F25 \ + MOVD ((offset)+(2*8))(R3), F26 \ + MOVD ((offset)+(3*8))(R3), F27 \ + MOVD ((offset)+(4*8))(R3), F28 \ + MOVD ((offset)+(5*8))(R3), F29 \ + MOVD ((offset)+(6*8))(R3), F30 \ + MOVD ((offset)+(7*8))(R3), F31 diff --git a/vendor/github.com/ebitengine/purego/cgo.go b/vendor/github.com/ebitengine/purego/cgo.go new file mode 100644 index 0000000..32bb256 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/cgo.go @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2022 The Ebitengine Authors + +//go:build cgo && (darwin || freebsd || linux || netbsd) + +package purego + +// if CGO_ENABLED=1 import the Cgo runtime to ensure that it is set up properly. +// This is required since some frameworks need TLS setup the C way which Go doesn't do. +// We currently don't support ios in fakecgo mode so force Cgo or fail +// Even if CGO_ENABLED=1 the Cgo runtime is not imported unless `import "C"` is used. +// which will import this package automatically. Normally this isn't an issue since it +// usually isn't possible to call into C without using that import. However, with purego +// it is since we don't use `import "C"`! +import ( + _ "runtime/cgo" + + _ "github.com/ebitengine/purego/internal/cgo" +) diff --git a/vendor/github.com/ebitengine/purego/dlerror.go b/vendor/github.com/ebitengine/purego/dlerror.go new file mode 100644 index 0000000..ad52b43 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/dlerror.go @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 The Ebitengine Authors + +//go:build darwin || freebsd || linux || netbsd + +package purego + +// Dlerror represents an error value returned from Dlopen, Dlsym, or Dlclose. +// +// This type is not available on Windows as there is no counterpart to it on Windows. +type Dlerror struct { + s string +} + +func (e Dlerror) Error() string { + return e.s +} diff --git a/vendor/github.com/ebitengine/purego/dlfcn.go b/vendor/github.com/ebitengine/purego/dlfcn.go new file mode 100644 index 0000000..2730d82 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/dlfcn.go @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2022 The Ebitengine Authors + +//go:build (darwin || freebsd || linux || netbsd) && !android && !faketime + +package purego + +import ( + "unsafe" +) + +// Unix Specification for dlfcn.h: https://pubs.opengroup.org/onlinepubs/7908799/xsh/dlfcn.h.html + +var ( + fnDlopen func(path string, mode int) uintptr + fnDlsym func(handle uintptr, name string) uintptr + fnDlerror func() string + fnDlclose func(handle uintptr) bool +) + +func init() { + RegisterFunc(&fnDlopen, dlopenABI0) + RegisterFunc(&fnDlsym, dlsymABI0) + RegisterFunc(&fnDlerror, dlerrorABI0) + RegisterFunc(&fnDlclose, dlcloseABI0) +} + +// Dlopen examines the dynamic library or bundle file specified by path. If the file is compatible +// with the current process and has not already been loaded into the +// current process, it is loaded and linked. After being linked, if it contains +// any initializer functions, they are called, before Dlopen +// returns. It returns a handle that can be used with Dlsym and Dlclose. +// A second call to Dlopen with the same path will return the same handle, but the internal +// reference count for the handle will be incremented. Therefore, all +// Dlopen calls should be balanced with a Dlclose call. +// +// This function is not available on Windows. +// Use [golang.org/x/sys/windows.LoadLibrary], [golang.org/x/sys/windows.LoadLibraryEx], +// [golang.org/x/sys/windows.NewLazyDLL], or [golang.org/x/sys/windows.NewLazySystemDLL] for Windows instead. +func Dlopen(path string, mode int) (uintptr, error) { + u := fnDlopen(path, mode) + if u == 0 { + return 0, Dlerror{fnDlerror()} + } + return u, nil +} + +// Dlsym takes a "handle" of a dynamic library returned by Dlopen and the symbol name. +// It returns the address where that symbol is loaded into memory. If the symbol is not found, +// in the specified library or any of the libraries that were automatically loaded by Dlopen +// when that library was loaded, Dlsym returns zero. +// +// This function is not available on Windows. +// Use [golang.org/x/sys/windows.GetProcAddress] for Windows instead. +func Dlsym(handle uintptr, name string) (uintptr, error) { + u := fnDlsym(handle, name) + if u == 0 { + return 0, Dlerror{fnDlerror()} + } + return u, nil +} + +// Dlclose decrements the reference count on the dynamic library handle. +// If the reference count drops to zero and no other loaded libraries +// use symbols in it, then the dynamic library is unloaded. +// +// This function is not available on Windows. +// Use [golang.org/x/sys/windows.FreeLibrary] for Windows instead. +func Dlclose(handle uintptr) error { + if fnDlclose(handle) { + return Dlerror{fnDlerror()} + } + return nil +} + +func loadSymbol(handle uintptr, name string) (uintptr, error) { + return Dlsym(handle, name) +} + +// these functions exist in dlfcn_stubs.s and are calling C functions linked to in dlfcn_GOOS.go +// the indirection is necessary because a function is actually a pointer to the pointer to the code. +// sadly, I do not know of anyway to remove the assembly stubs entirely because //go:linkname doesn't +// appear to work if you link directly to the C function on darwin arm64. + +//go:linkname dlopen dlopen +var dlopen uint8 +var dlopenABI0 = uintptr(unsafe.Pointer(&dlopen)) + +//go:linkname dlsym dlsym +var dlsym uint8 +var dlsymABI0 = uintptr(unsafe.Pointer(&dlsym)) + +//go:linkname dlclose dlclose +var dlclose uint8 +var dlcloseABI0 = uintptr(unsafe.Pointer(&dlclose)) + +//go:linkname dlerror dlerror +var dlerror uint8 +var dlerrorABI0 = uintptr(unsafe.Pointer(&dlerror)) diff --git a/vendor/github.com/ebitengine/purego/dlfcn_android.go b/vendor/github.com/ebitengine/purego/dlfcn_android.go new file mode 100644 index 0000000..0d53417 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/dlfcn_android.go @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2024 The Ebitengine Authors + +package purego + +import "github.com/ebitengine/purego/internal/cgo" + +// Source for constants: https://android.googlesource.com/platform/bionic/+/refs/heads/main/libc/include/dlfcn.h + +const ( + is64bit = 1 << (^uintptr(0) >> 63) / 2 + is32bit = 1 - is64bit + RTLD_DEFAULT = is32bit * 0xffffffff + RTLD_LAZY = 0x00000001 + RTLD_NOW = is64bit * 0x00000002 + RTLD_LOCAL = 0x00000000 + RTLD_GLOBAL = is64bit*0x00100 | is32bit*0x00000002 +) + +func Dlopen(path string, mode int) (uintptr, error) { + return cgo.Dlopen(path, mode) +} + +func Dlsym(handle uintptr, name string) (uintptr, error) { + return cgo.Dlsym(handle, name) +} + +func Dlclose(handle uintptr) error { + return cgo.Dlclose(handle) +} + +func loadSymbol(handle uintptr, name string) (uintptr, error) { + return Dlsym(handle, name) +} diff --git a/vendor/github.com/ebitengine/purego/dlfcn_darwin.go b/vendor/github.com/ebitengine/purego/dlfcn_darwin.go new file mode 100644 index 0000000..27f5607 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/dlfcn_darwin.go @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2022 The Ebitengine Authors + +package purego + +// Source for constants: https://opensource.apple.com/source/dyld/dyld-360.14/include/dlfcn.h.auto.html + +const ( + RTLD_DEFAULT = 1<<64 - 2 // Pseudo-handle for dlsym so search for any loaded symbol + RTLD_LAZY = 0x1 // Relocations are performed at an implementation-dependent time. + RTLD_NOW = 0x2 // Relocations are performed when the object is loaded. + RTLD_LOCAL = 0x4 // All symbols are not made available for relocation processing by other modules. + RTLD_GLOBAL = 0x8 // All symbols are available for relocation processing of other modules. +) + +//go:cgo_import_dynamic purego_dlopen dlopen "/usr/lib/libSystem.B.dylib" +//go:cgo_import_dynamic purego_dlsym dlsym "/usr/lib/libSystem.B.dylib" +//go:cgo_import_dynamic purego_dlerror dlerror "/usr/lib/libSystem.B.dylib" +//go:cgo_import_dynamic purego_dlclose dlclose "/usr/lib/libSystem.B.dylib" diff --git a/vendor/github.com/ebitengine/purego/dlfcn_freebsd.go b/vendor/github.com/ebitengine/purego/dlfcn_freebsd.go new file mode 100644 index 0000000..6b37162 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/dlfcn_freebsd.go @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2022 The Ebitengine Authors + +package purego + +// Constants as defined in https://github.com/freebsd/freebsd-src/blob/main/include/dlfcn.h +const ( + intSize = 32 << (^uint(0) >> 63) // 32 or 64 + RTLD_DEFAULT = 1<> 63) // 32 or 64 + RTLD_DEFAULT = 1< C) +// +// string <=> char* +// bool <=> _Bool +// uintptr <=> uintptr_t +// uint <=> uint32_t or uint64_t +// uint8 <=> uint8_t +// uint16 <=> uint16_t +// uint32 <=> uint32_t +// uint64 <=> uint64_t +// int <=> int32_t or int64_t +// int8 <=> int8_t +// int16 <=> int16_t +// int32 <=> int32_t +// int64 <=> int64_t +// float32 <=> float +// float64 <=> double +// struct <=> struct (WIP - darwin only) +// func <=> C function +// unsafe.Pointer, *T <=> void* +// []T => void* +// +// There is a special case when the last argument of fptr is a variadic interface (or []interface} +// it will be expanded into a call to the C function as if it had the arguments in that slice. +// This means that using arg ...any is like a cast to the function with the arguments inside arg. +// This is not the same as C variadic. +// +// # Memory +// +// In general it is not possible for purego to guarantee the lifetimes of objects returned or received from +// calling functions using RegisterFunc. For arguments to a C function it is important that the C function doesn't +// hold onto a reference to Go memory. This is the same as the [Cgo rules]. +// +// However, there are some special cases. When passing a string as an argument if the string does not end in a null +// terminated byte (\x00) then the string will be copied into memory maintained by purego. The memory is only valid for +// that specific call. Therefore, if the C code keeps a reference to that string it may become invalid at some +// undefined time. However, if the string does already contain a null-terminated byte then no copy is done. +// It is then the responsibility of the caller to ensure the string stays alive as long as it's needed in C memory. +// This can be done using runtime.KeepAlive or allocating the string in C memory using malloc. When a C function +// returns a null-terminated pointer to char a Go string can be used. Purego will allocate a new string in Go memory +// and copy the data over. This string will be garbage collected whenever Go decides it's no longer referenced. +// This C created string will not be freed by purego. If the pointer to char is not null-terminated or must continue +// to point to C memory (because it's a buffer for example) then use a pointer to byte and then convert that to a slice +// using unsafe.Slice. Doing this means that it becomes the responsibility of the caller to care about the lifetime +// of the pointer +// +// # Structs +// +// Purego can handle the most common structs that have fields of builtin types like int8, uint16, float32, etc. However, +// it does not support aligning fields properly. It is therefore the responsibility of the caller to ensure +// that all padding is added to the Go struct to match the C one. See `BoolStructFn` in struct_test.go for an example. +// +// # Example +// +// All functions below call this C function: +// +// char *foo(char *str); +// +// // Let purego convert types +// var foo func(s string) string +// goString := foo("copied") +// // Go will garbage collect this string +// +// // Manually, handle allocations +// var foo2 func(b string) *byte +// mustFree := foo2("not copied\x00") +// defer free(mustFree) +// +// [Cgo rules]: https://pkg.go.dev/cmd/cgo#hdr-Go_references_to_C +func RegisterFunc(fptr any, cfn uintptr) { + fn := reflect.ValueOf(fptr).Elem() + ty := fn.Type() + if ty.Kind() != reflect.Func { + panic("purego: fptr must be a function pointer") + } + if ty.NumOut() > 1 { + panic("purego: function can only return zero or one values") + } + if cfn == 0 { + panic("purego: cfn is nil") + } + if ty.NumOut() == 1 && (ty.Out(0).Kind() == reflect.Float32 || ty.Out(0).Kind() == reflect.Float64) && + runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" { + panic("purego: float returns are not supported") + } + { + // this code checks how many registers and stack this function will use + // to avoid crashing with too many arguments + var ints int + var floats int + var stack int + for i := 0; i < ty.NumIn(); i++ { + arg := ty.In(i) + switch arg.Kind() { + case reflect.Func: + // This only does preliminary testing to ensure the CDecl argument + // is the first argument. Full testing is done when the callback is actually + // created in NewCallback. + for j := 0; j < arg.NumIn(); j++ { + in := arg.In(j) + if !in.AssignableTo(reflect.TypeOf(CDecl{})) { + continue + } + if j != 0 { + panic("purego: CDecl must be the first argument") + } + } + case reflect.String, reflect.Uintptr, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Ptr, reflect.UnsafePointer, + reflect.Slice, reflect.Bool: + if ints < numOfIntegerRegisters() { + ints++ + } else { + stack++ + } + case reflect.Float32, reflect.Float64: + const is32bit = unsafe.Sizeof(uintptr(0)) == 4 + if is32bit { + panic("purego: floats only supported on 64bit platforms") + } + if floats < numOfFloatRegisters { + floats++ + } else { + stack++ + } + case reflect.Struct: + if runtime.GOOS != "darwin" || (runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64") { + panic("purego: struct arguments are only supported on darwin amd64 & arm64") + } + if arg.Size() == 0 { + continue + } + addInt := func(u uintptr) { + ints++ + } + addFloat := func(u uintptr) { + floats++ + } + addStack := func(u uintptr) { + stack++ + } + _ = addStruct(reflect.New(arg).Elem(), &ints, &floats, &stack, addInt, addFloat, addStack, nil) + default: + panic("purego: unsupported kind " + arg.Kind().String()) + } + } + if ty.NumOut() == 1 && ty.Out(0).Kind() == reflect.Struct { + if runtime.GOOS != "darwin" { + panic("purego: struct return values only supported on darwin arm64 & amd64") + } + outType := ty.Out(0) + checkStructFieldsSupported(outType) + if runtime.GOARCH == "amd64" && outType.Size() > maxRegAllocStructSize { + // on amd64 if struct is bigger than 16 bytes allocate the return struct + // and pass it in as a hidden first argument. + ints++ + } + } + sizeOfStack := maxArgs - numOfIntegerRegisters() + if stack > sizeOfStack { + panic("purego: too many arguments") + } + } + v := reflect.MakeFunc(ty, func(args []reflect.Value) (results []reflect.Value) { + var sysargs [maxArgs]uintptr + var floats [numOfFloatRegisters]uintptr + var numInts int + var numFloats int + var numStack int + var addStack, addInt, addFloat func(x uintptr) + if runtime.GOARCH == "arm64" || runtime.GOOS != "windows" { + // Windows arm64 uses the same calling convention as macOS and Linux + addStack = func(x uintptr) { + sysargs[numOfIntegerRegisters()+numStack] = x + numStack++ + } + addInt = func(x uintptr) { + if numInts >= numOfIntegerRegisters() { + addStack(x) + } else { + sysargs[numInts] = x + numInts++ + } + } + addFloat = func(x uintptr) { + if numFloats < len(floats) { + floats[numFloats] = x + numFloats++ + } else { + addStack(x) + } + } + } else { + // On Windows amd64 the arguments are passed in the numbered registered. + // So the first int is in the first integer register and the first float + // is in the second floating register if there is already a first int. + // This is in contrast to how macOS and Linux pass arguments which + // tries to use as many registers as possible in the calling convention. + addStack = func(x uintptr) { + sysargs[numStack] = x + numStack++ + } + addInt = addStack + addFloat = addStack + } + + var keepAlive []any + defer func() { + runtime.KeepAlive(keepAlive) + runtime.KeepAlive(args) + }() + + var arm64_r8 uintptr + if ty.NumOut() == 1 && ty.Out(0).Kind() == reflect.Struct { + outType := ty.Out(0) + if (runtime.GOARCH == "amd64" || runtime.GOARCH == "loong64") && outType.Size() > maxRegAllocStructSize { + val := reflect.New(outType) + keepAlive = append(keepAlive, val) + addInt(val.Pointer()) + } else if runtime.GOARCH == "arm64" && outType.Size() > maxRegAllocStructSize { + isAllFloats, numFields := isAllSameFloat(outType) + if !isAllFloats || numFields > 4 { + val := reflect.New(outType) + keepAlive = append(keepAlive, val) + arm64_r8 = val.Pointer() + } + } + } + for i, v := range args { + if variadic, ok := args[i].Interface().([]any); ok { + if i != len(args)-1 { + panic("purego: can only expand last parameter") + } + for _, x := range variadic { + keepAlive = addValue(reflect.ValueOf(x), keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack) + } + continue + } + if runtime.GOARCH == "arm64" && runtime.GOOS == "darwin" && + (numInts >= numOfIntegerRegisters() || numFloats >= numOfFloatRegisters) && v.Kind() != reflect.Struct { // hit the stack + fields := make([]reflect.StructField, len(args[i:])) + + for j, val := range args[i:] { + if val.Kind() == reflect.String { + ptr := strings.CString(v.String()) + keepAlive = append(keepAlive, ptr) + val = reflect.ValueOf(ptr) + args[i+j] = val + } + fields[j] = reflect.StructField{ + Name: "X" + strconv.Itoa(j), + Type: val.Type(), + } + } + structType := reflect.StructOf(fields) + structInstance := reflect.New(structType).Elem() + for j, val := range args[i:] { + structInstance.Field(j).Set(val) + } + placeRegisters(structInstance, addFloat, addInt) + break + } + keepAlive = addValue(v, keepAlive, addInt, addFloat, addStack, &numInts, &numFloats, &numStack) + } + + syscall := thePool.Get().(*syscall15Args) + defer thePool.Put(syscall) + + if runtime.GOARCH == "loong64" { + *syscall = syscall15Args{ + cfn, + sysargs[0], sysargs[1], sysargs[2], sysargs[3], sysargs[4], sysargs[5], + sysargs[6], sysargs[7], sysargs[8], sysargs[9], sysargs[10], sysargs[11], + sysargs[12], sysargs[13], sysargs[14], + floats[0], floats[1], floats[2], floats[3], floats[4], floats[5], floats[6], floats[7], + 0, + } + runtime_cgocall(syscall15XABI0, unsafe.Pointer(syscall)) + } else if runtime.GOARCH == "arm64" || runtime.GOOS != "windows" { + // Use the normal arm64 calling convention even on Windows + *syscall = syscall15Args{ + cfn, + sysargs[0], sysargs[1], sysargs[2], sysargs[3], sysargs[4], sysargs[5], + sysargs[6], sysargs[7], sysargs[8], sysargs[9], sysargs[10], sysargs[11], + sysargs[12], sysargs[13], sysargs[14], + floats[0], floats[1], floats[2], floats[3], floats[4], floats[5], floats[6], floats[7], + arm64_r8, + } + runtime_cgocall(syscall15XABI0, unsafe.Pointer(syscall)) + } else { + *syscall = syscall15Args{} + // This is a fallback for Windows amd64, 386, and arm. Note this may not support floats + syscall.a1, syscall.a2, _ = syscall_syscall15X(cfn, sysargs[0], sysargs[1], sysargs[2], sysargs[3], sysargs[4], + sysargs[5], sysargs[6], sysargs[7], sysargs[8], sysargs[9], sysargs[10], sysargs[11], + sysargs[12], sysargs[13], sysargs[14]) + syscall.f1 = syscall.a2 // on amd64 a2 stores the float return. On 32bit platforms floats aren't support + } + if ty.NumOut() == 0 { + return nil + } + outType := ty.Out(0) + v := reflect.New(outType).Elem() + switch outType.Kind() { + case reflect.Uintptr, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + v.SetUint(uint64(syscall.a1)) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + v.SetInt(int64(syscall.a1)) + case reflect.Bool: + v.SetBool(byte(syscall.a1) != 0) + case reflect.UnsafePointer: + // We take the address and then dereference it to trick go vet from creating a possible miss-use of unsafe.Pointer + v.SetPointer(*(*unsafe.Pointer)(unsafe.Pointer(&syscall.a1))) + case reflect.Ptr: + v = reflect.NewAt(outType, unsafe.Pointer(&syscall.a1)).Elem() + case reflect.Func: + // wrap this C function in a nicely typed Go function + v = reflect.New(outType) + RegisterFunc(v.Interface(), syscall.a1) + case reflect.String: + v.SetString(strings.GoString(syscall.a1)) + case reflect.Float32: + // NOTE: syscall.r2 is only the floating return value on 64bit platforms. + // On 32bit platforms syscall.r2 is the upper part of a 64bit return. + v.SetFloat(float64(math.Float32frombits(uint32(syscall.f1)))) + case reflect.Float64: + // NOTE: syscall.r2 is only the floating return value on 64bit platforms. + // On 32bit platforms syscall.r2 is the upper part of a 64bit return. + v.SetFloat(math.Float64frombits(uint64(syscall.f1))) + case reflect.Struct: + v = getStruct(outType, *syscall) + default: + panic("purego: unsupported return kind: " + outType.Kind().String()) + } + if len(args) > 0 { + // reuse args slice instead of allocating one when possible + args[0] = v + return args[:1] + } else { + return []reflect.Value{v} + } + }) + fn.Set(v) +} + +func addValue(v reflect.Value, keepAlive []any, addInt func(x uintptr), addFloat func(x uintptr), addStack func(x uintptr), numInts *int, numFloats *int, numStack *int) []any { + switch v.Kind() { + case reflect.String: + ptr := strings.CString(v.String()) + keepAlive = append(keepAlive, ptr) + addInt(uintptr(unsafe.Pointer(ptr))) + case reflect.Uintptr, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + addInt(uintptr(v.Uint())) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + addInt(uintptr(v.Int())) + case reflect.Ptr, reflect.UnsafePointer, reflect.Slice: + // There is no need to keepAlive this pointer separately because it is kept alive in the args variable + addInt(v.Pointer()) + case reflect.Func: + addInt(NewCallback(v.Interface())) + case reflect.Bool: + if v.Bool() { + addInt(1) + } else { + addInt(0) + } + case reflect.Float32: + addFloat(uintptr(math.Float32bits(float32(v.Float())))) + case reflect.Float64: + addFloat(uintptr(math.Float64bits(v.Float()))) + case reflect.Struct: + keepAlive = addStruct(v, numInts, numFloats, numStack, addInt, addFloat, addStack, keepAlive) + default: + panic("purego: unsupported kind: " + v.Kind().String()) + } + return keepAlive +} + +// maxRegAllocStructSize is the biggest a struct can be while still fitting in registers. +// if it is bigger than this than enough space must be allocated on the heap and then passed into +// the function as the first parameter on amd64 or in R8 on arm64. +// +// If you change this make sure to update it in objc_runtime_darwin.go +const maxRegAllocStructSize = 16 + +func isAllSameFloat(ty reflect.Type) (allFloats bool, numFields int) { + allFloats = true + root := ty.Field(0).Type + for root.Kind() == reflect.Struct { + root = root.Field(0).Type + } + first := root.Kind() + if first != reflect.Float32 && first != reflect.Float64 { + allFloats = false + } + for i := 0; i < ty.NumField(); i++ { + f := ty.Field(i).Type + if f.Kind() == reflect.Struct { + var structNumFields int + allFloats, structNumFields = isAllSameFloat(f) + numFields += structNumFields + continue + } + numFields++ + if f.Kind() != first { + allFloats = false + } + } + return allFloats, numFields +} + +func checkStructFieldsSupported(ty reflect.Type) { + for i := 0; i < ty.NumField(); i++ { + f := ty.Field(i).Type + if f.Kind() == reflect.Array { + f = f.Elem() + } else if f.Kind() == reflect.Struct { + checkStructFieldsSupported(f) + continue + } + switch f.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Uintptr, reflect.Ptr, reflect.UnsafePointer, reflect.Float64, reflect.Float32: + default: + panic(fmt.Sprintf("purego: struct field type %s is not supported", f)) + } + } +} + +func roundUpTo8(val uintptr) uintptr { + return (val + 7) &^ 7 +} + +func numOfIntegerRegisters() int { + switch runtime.GOARCH { + case "arm64", "loong64": + return 8 + case "amd64": + return 6 + default: + // since this platform isn't supported and can therefore only access + // integer registers it is fine to return the maxArgs + return maxArgs + } +} diff --git a/vendor/github.com/ebitengine/purego/gen.go b/vendor/github.com/ebitengine/purego/gen.go new file mode 100644 index 0000000..9cb7c45 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/gen.go @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 The Ebitengine Authors + +package purego + +//go:generate go run wincallback.go diff --git a/vendor/github.com/ebitengine/purego/go_runtime.go b/vendor/github.com/ebitengine/purego/go_runtime.go new file mode 100644 index 0000000..b327f78 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/go_runtime.go @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2022 The Ebitengine Authors + +//go:build darwin || freebsd || linux || netbsd || windows + +package purego + +import ( + "unsafe" +) + +//go:linkname runtime_cgocall runtime.cgocall +func runtime_cgocall(fn uintptr, arg unsafe.Pointer) int32 // from runtime/sys_libc.go diff --git a/vendor/github.com/ebitengine/purego/internal/cgo/dlfcn_cgo_unix.go b/vendor/github.com/ebitengine/purego/internal/cgo/dlfcn_cgo_unix.go new file mode 100644 index 0000000..6d0571a --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/cgo/dlfcn_cgo_unix.go @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2024 The Ebitengine Authors + +//go:build freebsd || linux || netbsd + +package cgo + +/* +#cgo !netbsd LDFLAGS: -ldl + +#include +#include +*/ +import "C" + +import ( + "errors" + "unsafe" +) + +func Dlopen(filename string, flag int) (uintptr, error) { + cfilename := C.CString(filename) + defer C.free(unsafe.Pointer(cfilename)) + handle := C.dlopen(cfilename, C.int(flag)) + if handle == nil { + return 0, errors.New(C.GoString(C.dlerror())) + } + return uintptr(handle), nil +} + +func Dlsym(handle uintptr, symbol string) (uintptr, error) { + csymbol := C.CString(symbol) + defer C.free(unsafe.Pointer(csymbol)) + symbolAddr := C.dlsym(*(*unsafe.Pointer)(unsafe.Pointer(&handle)), csymbol) + if symbolAddr == nil { + return 0, errors.New(C.GoString(C.dlerror())) + } + return uintptr(symbolAddr), nil +} + +func Dlclose(handle uintptr) error { + result := C.dlclose(*(*unsafe.Pointer)(unsafe.Pointer(&handle))) + if result != 0 { + return errors.New(C.GoString(C.dlerror())) + } + return nil +} + +// all that is needed is to assign each dl function because then its +// symbol will then be made available to the linker and linked to inside dlfcn.go +var ( + _ = C.dlopen + _ = C.dlsym + _ = C.dlerror + _ = C.dlclose +) diff --git a/vendor/github.com/ebitengine/purego/internal/cgo/empty.go b/vendor/github.com/ebitengine/purego/internal/cgo/empty.go new file mode 100644 index 0000000..1d7cffe --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/cgo/empty.go @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2024 The Ebitengine Authors + +package cgo + +// Empty so that importing this package doesn't cause issue for certain platforms. diff --git a/vendor/github.com/ebitengine/purego/internal/cgo/syscall_cgo_unix.go b/vendor/github.com/ebitengine/purego/internal/cgo/syscall_cgo_unix.go new file mode 100644 index 0000000..10393fe --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/cgo/syscall_cgo_unix.go @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2022 The Ebitengine Authors + +//go:build freebsd || (linux && !(arm64 || amd64 || loong64)) || netbsd + +package cgo + +// this file is placed inside internal/cgo and not package purego +// because Cgo and assembly files can't be in the same package. + +/* +#cgo !netbsd LDFLAGS: -ldl + +#include +#include +#include +#include + +typedef struct syscall15Args { + uintptr_t fn; + uintptr_t a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15; + uintptr_t f1, f2, f3, f4, f5, f6, f7, f8; + uintptr_t err; +} syscall15Args; + +void syscall15(struct syscall15Args *args) { + assert((args->f1|args->f2|args->f3|args->f4|args->f5|args->f6|args->f7|args->f8) == 0); + uintptr_t (*func_name)(uintptr_t a1, uintptr_t a2, uintptr_t a3, uintptr_t a4, uintptr_t a5, uintptr_t a6, + uintptr_t a7, uintptr_t a8, uintptr_t a9, uintptr_t a10, uintptr_t a11, uintptr_t a12, + uintptr_t a13, uintptr_t a14, uintptr_t a15); + *(void**)(&func_name) = (void*)(args->fn); + uintptr_t r1 = func_name(args->a1,args->a2,args->a3,args->a4,args->a5,args->a6,args->a7,args->a8,args->a9, + args->a10,args->a11,args->a12,args->a13,args->a14,args->a15); + args->a1 = r1; + args->err = errno; +} + +*/ +import "C" +import "unsafe" + +// assign purego.syscall15XABI0 to the C version of this function. +var Syscall15XABI0 = unsafe.Pointer(C.syscall15) + +//go:nosplit +func Syscall15X(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 uintptr) (r1, r2, err uintptr) { + args := C.syscall15Args{ + C.uintptr_t(fn), C.uintptr_t(a1), C.uintptr_t(a2), C.uintptr_t(a3), + C.uintptr_t(a4), C.uintptr_t(a5), C.uintptr_t(a6), + C.uintptr_t(a7), C.uintptr_t(a8), C.uintptr_t(a9), C.uintptr_t(a10), C.uintptr_t(a11), C.uintptr_t(a12), + C.uintptr_t(a13), C.uintptr_t(a14), C.uintptr_t(a15), 0, 0, 0, 0, 0, 0, 0, 0, 0, + } + C.syscall15(&args) + return uintptr(args.a1), 0, uintptr(args.err) +} diff --git a/vendor/github.com/ebitengine/purego/internal/fakecgo/abi_amd64.h b/vendor/github.com/ebitengine/purego/internal/fakecgo/abi_amd64.h new file mode 100644 index 0000000..9949435 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/fakecgo/abi_amd64.h @@ -0,0 +1,99 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Macros for transitioning from the host ABI to Go ABI0. +// +// These save the frame pointer, so in general, functions that use +// these should have zero frame size to suppress the automatic frame +// pointer, though it's harmless to not do this. + +#ifdef GOOS_windows + +// REGS_HOST_TO_ABI0_STACK is the stack bytes used by +// PUSH_REGS_HOST_TO_ABI0. +#define REGS_HOST_TO_ABI0_STACK (28*8 + 8) + +// PUSH_REGS_HOST_TO_ABI0 prepares for transitioning from +// the host ABI to Go ABI0 code. It saves all registers that are +// callee-save in the host ABI and caller-save in Go ABI0 and prepares +// for entry to Go. +// +// Save DI SI BP BX R12 R13 R14 R15 X6-X15 registers and the DF flag. +// Clear the DF flag for the Go ABI. +// MXCSR matches the Go ABI, so we don't have to set that, +// and Go doesn't modify it, so we don't have to save it. +#define PUSH_REGS_HOST_TO_ABI0() \ + PUSHFQ \ + CLD \ + ADJSP $(REGS_HOST_TO_ABI0_STACK - 8) \ + MOVQ DI, (0*0)(SP) \ + MOVQ SI, (1*8)(SP) \ + MOVQ BP, (2*8)(SP) \ + MOVQ BX, (3*8)(SP) \ + MOVQ R12, (4*8)(SP) \ + MOVQ R13, (5*8)(SP) \ + MOVQ R14, (6*8)(SP) \ + MOVQ R15, (7*8)(SP) \ + MOVUPS X6, (8*8)(SP) \ + MOVUPS X7, (10*8)(SP) \ + MOVUPS X8, (12*8)(SP) \ + MOVUPS X9, (14*8)(SP) \ + MOVUPS X10, (16*8)(SP) \ + MOVUPS X11, (18*8)(SP) \ + MOVUPS X12, (20*8)(SP) \ + MOVUPS X13, (22*8)(SP) \ + MOVUPS X14, (24*8)(SP) \ + MOVUPS X15, (26*8)(SP) + +#define POP_REGS_HOST_TO_ABI0() \ + MOVQ (0*0)(SP), DI \ + MOVQ (1*8)(SP), SI \ + MOVQ (2*8)(SP), BP \ + MOVQ (3*8)(SP), BX \ + MOVQ (4*8)(SP), R12 \ + MOVQ (5*8)(SP), R13 \ + MOVQ (6*8)(SP), R14 \ + MOVQ (7*8)(SP), R15 \ + MOVUPS (8*8)(SP), X6 \ + MOVUPS (10*8)(SP), X7 \ + MOVUPS (12*8)(SP), X8 \ + MOVUPS (14*8)(SP), X9 \ + MOVUPS (16*8)(SP), X10 \ + MOVUPS (18*8)(SP), X11 \ + MOVUPS (20*8)(SP), X12 \ + MOVUPS (22*8)(SP), X13 \ + MOVUPS (24*8)(SP), X14 \ + MOVUPS (26*8)(SP), X15 \ + ADJSP $-(REGS_HOST_TO_ABI0_STACK - 8) \ + POPFQ + +#else +// SysV ABI + +#define REGS_HOST_TO_ABI0_STACK (6*8) + +// SysV MXCSR matches the Go ABI, so we don't have to set that, +// and Go doesn't modify it, so we don't have to save it. +// Both SysV and Go require DF to be cleared, so that's already clear. +// The SysV and Go frame pointer conventions are compatible. +#define PUSH_REGS_HOST_TO_ABI0() \ + ADJSP $(REGS_HOST_TO_ABI0_STACK) \ + MOVQ BP, (5*8)(SP) \ + LEAQ (5*8)(SP), BP \ + MOVQ BX, (0*8)(SP) \ + MOVQ R12, (1*8)(SP) \ + MOVQ R13, (2*8)(SP) \ + MOVQ R14, (3*8)(SP) \ + MOVQ R15, (4*8)(SP) + +#define POP_REGS_HOST_TO_ABI0() \ + MOVQ (0*8)(SP), BX \ + MOVQ (1*8)(SP), R12 \ + MOVQ (2*8)(SP), R13 \ + MOVQ (3*8)(SP), R14 \ + MOVQ (4*8)(SP), R15 \ + MOVQ (5*8)(SP), BP \ + ADJSP $-(REGS_HOST_TO_ABI0_STACK) + +#endif diff --git a/vendor/github.com/ebitengine/purego/internal/fakecgo/abi_arm64.h b/vendor/github.com/ebitengine/purego/internal/fakecgo/abi_arm64.h new file mode 100644 index 0000000..5d5061e --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/fakecgo/abi_arm64.h @@ -0,0 +1,39 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Macros for transitioning from the host ABI to Go ABI0. +// +// These macros save and restore the callee-saved registers +// from the stack, but they don't adjust stack pointer, so +// the user should prepare stack space in advance. +// SAVE_R19_TO_R28(offset) saves R19 ~ R28 to the stack space +// of ((offset)+0*8)(RSP) ~ ((offset)+9*8)(RSP). +// +// SAVE_F8_TO_F15(offset) saves F8 ~ F15 to the stack space +// of ((offset)+0*8)(RSP) ~ ((offset)+7*8)(RSP). +// +// R29 is not saved because Go will save and restore it. + +#define SAVE_R19_TO_R28(offset) \ + STP (R19, R20), ((offset)+0*8)(RSP) \ + STP (R21, R22), ((offset)+2*8)(RSP) \ + STP (R23, R24), ((offset)+4*8)(RSP) \ + STP (R25, R26), ((offset)+6*8)(RSP) \ + STP (R27, g), ((offset)+8*8)(RSP) +#define RESTORE_R19_TO_R28(offset) \ + LDP ((offset)+0*8)(RSP), (R19, R20) \ + LDP ((offset)+2*8)(RSP), (R21, R22) \ + LDP ((offset)+4*8)(RSP), (R23, R24) \ + LDP ((offset)+6*8)(RSP), (R25, R26) \ + LDP ((offset)+8*8)(RSP), (R27, g) /* R28 */ +#define SAVE_F8_TO_F15(offset) \ + FSTPD (F8, F9), ((offset)+0*8)(RSP) \ + FSTPD (F10, F11), ((offset)+2*8)(RSP) \ + FSTPD (F12, F13), ((offset)+4*8)(RSP) \ + FSTPD (F14, F15), ((offset)+6*8)(RSP) +#define RESTORE_F8_TO_F15(offset) \ + FLDPD ((offset)+0*8)(RSP), (F8, F9) \ + FLDPD ((offset)+2*8)(RSP), (F10, F11) \ + FLDPD ((offset)+4*8)(RSP), (F12, F13) \ + FLDPD ((offset)+6*8)(RSP), (F14, F15) diff --git a/vendor/github.com/ebitengine/purego/internal/fakecgo/abi_loong64.h b/vendor/github.com/ebitengine/purego/internal/fakecgo/abi_loong64.h new file mode 100644 index 0000000..b10d837 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/fakecgo/abi_loong64.h @@ -0,0 +1,60 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Macros for transitioning from the host ABI to Go ABI0. +// +// These macros save and restore the callee-saved registers +// from the stack, but they don't adjust stack pointer, so +// the user should prepare stack space in advance. +// SAVE_R22_TO_R31(offset) saves R22 ~ R31 to the stack space +// of ((offset)+0*8)(R3) ~ ((offset)+9*8)(R3). +// +// SAVE_F24_TO_F31(offset) saves F24 ~ F31 to the stack space +// of ((offset)+0*8)(R3) ~ ((offset)+7*8)(R3). +// +// Note: g is R22 + +#define SAVE_R22_TO_R31(offset) \ + MOVV g, ((offset)+(0*8))(R3) \ + MOVV R23, ((offset)+(1*8))(R3) \ + MOVV R24, ((offset)+(2*8))(R3) \ + MOVV R25, ((offset)+(3*8))(R3) \ + MOVV R26, ((offset)+(4*8))(R3) \ + MOVV R27, ((offset)+(5*8))(R3) \ + MOVV R28, ((offset)+(6*8))(R3) \ + MOVV R29, ((offset)+(7*8))(R3) \ + MOVV R30, ((offset)+(8*8))(R3) \ + MOVV R31, ((offset)+(9*8))(R3) + +#define SAVE_F24_TO_F31(offset) \ + MOVD F24, ((offset)+(0*8))(R3) \ + MOVD F25, ((offset)+(1*8))(R3) \ + MOVD F26, ((offset)+(2*8))(R3) \ + MOVD F27, ((offset)+(3*8))(R3) \ + MOVD F28, ((offset)+(4*8))(R3) \ + MOVD F29, ((offset)+(5*8))(R3) \ + MOVD F30, ((offset)+(6*8))(R3) \ + MOVD F31, ((offset)+(7*8))(R3) + +#define RESTORE_R22_TO_R31(offset) \ + MOVV ((offset)+(0*8))(R3), g \ + MOVV ((offset)+(1*8))(R3), R23 \ + MOVV ((offset)+(2*8))(R3), R24 \ + MOVV ((offset)+(3*8))(R3), R25 \ + MOVV ((offset)+(4*8))(R3), R26 \ + MOVV ((offset)+(5*8))(R3), R27 \ + MOVV ((offset)+(6*8))(R3), R28 \ + MOVV ((offset)+(7*8))(R3), R29 \ + MOVV ((offset)+(8*8))(R3), R30 \ + MOVV ((offset)+(9*8))(R3), R31 + +#define RESTORE_F24_TO_F31(offset) \ + MOVD ((offset)+(0*8))(R3), F24 \ + MOVD ((offset)+(1*8))(R3), F25 \ + MOVD ((offset)+(2*8))(R3), F26 \ + MOVD ((offset)+(3*8))(R3), F27 \ + MOVD ((offset)+(4*8))(R3), F28 \ + MOVD ((offset)+(5*8))(R3), F29 \ + MOVD ((offset)+(6*8))(R3), F30 \ + MOVD ((offset)+(7*8))(R3), F31 diff --git a/vendor/github.com/ebitengine/purego/internal/fakecgo/asm_amd64.s b/vendor/github.com/ebitengine/purego/internal/fakecgo/asm_amd64.s new file mode 100644 index 0000000..2b7eb57 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/fakecgo/asm_amd64.s @@ -0,0 +1,39 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "textflag.h" +#include "abi_amd64.h" + +// Called by C code generated by cmd/cgo. +// func crosscall2(fn, a unsafe.Pointer, n int32, ctxt uintptr) +// Saves C callee-saved registers and calls cgocallback with three arguments. +// fn is the PC of a func(a unsafe.Pointer) function. +// This signature is known to SWIG, so we can't change it. +TEXT crosscall2(SB), NOSPLIT, $0-0 + PUSH_REGS_HOST_TO_ABI0() + + // Make room for arguments to cgocallback. + ADJSP $0x18 + +#ifndef GOOS_windows + MOVQ DI, 0x0(SP) // fn + MOVQ SI, 0x8(SP) // arg + + // Skip n in DX. + MOVQ CX, 0x10(SP) // ctxt + +#else + MOVQ CX, 0x0(SP) // fn + MOVQ DX, 0x8(SP) // arg + + // Skip n in R8. + MOVQ R9, 0x10(SP) // ctxt + +#endif + + CALL runtime·cgocallback(SB) + + ADJSP $-0x18 + POP_REGS_HOST_TO_ABI0() + RET diff --git a/vendor/github.com/ebitengine/purego/internal/fakecgo/asm_arm64.s b/vendor/github.com/ebitengine/purego/internal/fakecgo/asm_arm64.s new file mode 100644 index 0000000..50e5261 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/fakecgo/asm_arm64.s @@ -0,0 +1,36 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "textflag.h" +#include "abi_arm64.h" + +// Called by C code generated by cmd/cgo. +// func crosscall2(fn, a unsafe.Pointer, n int32, ctxt uintptr) +// Saves C callee-saved registers and calls cgocallback with three arguments. +// fn is the PC of a func(a unsafe.Pointer) function. +TEXT crosscall2(SB), NOSPLIT|NOFRAME, $0 +/* + * We still need to save all callee save register as before, and then + * push 3 args for fn (R0, R1, R3), skipping R2. + * Also note that at procedure entry in gc world, 8(RSP) will be the + * first arg. + */ + SUB $(8*24), RSP + STP (R0, R1), (8*1)(RSP) + MOVD R3, (8*3)(RSP) + + SAVE_R19_TO_R28(8*4) + SAVE_F8_TO_F15(8*14) + STP (R29, R30), (8*22)(RSP) + + // Initialize Go ABI environment + BL runtime·load_g(SB) + BL runtime·cgocallback(SB) + + RESTORE_R19_TO_R28(8*4) + RESTORE_F8_TO_F15(8*14) + LDP (8*22)(RSP), (R29, R30) + + ADD $(8*24), RSP + RET diff --git a/vendor/github.com/ebitengine/purego/internal/fakecgo/asm_loong64.s b/vendor/github.com/ebitengine/purego/internal/fakecgo/asm_loong64.s new file mode 100644 index 0000000..aea4f8e --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/fakecgo/asm_loong64.s @@ -0,0 +1,40 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "textflag.h" +#include "abi_loong64.h" + +// Called by C code generated by cmd/cgo. +// func crosscall2(fn, a unsafe.Pointer, n int32, ctxt uintptr) +// Saves C callee-saved registers and calls cgocallback with three arguments. +// fn is the PC of a func(a unsafe.Pointer) function. +TEXT crosscall2(SB),NOSPLIT|NOFRAME,$0 + /* + * We still need to save all callee save register as before, and then + * push 3 args for fn (R4, R5, R7), skipping R6. + * Also note that at procedure entry in gc world, 8(R29) will be the + * first arg. + */ + + ADDV $(-23*8), R3 + MOVV R4, (1*8)(R3) // fn unsafe.Pointer + MOVV R5, (2*8)(R3) // a unsafe.Pointer + MOVV R7, (3*8)(R3) // ctxt uintptr + + SAVE_R22_TO_R31((4*8)) + SAVE_F24_TO_F31((14*8)) + MOVV R1, (22*8)(R3) + + // Initialize Go ABI environment + JAL runtime·load_g(SB) + + JAL runtime·cgocallback(SB) + + RESTORE_R22_TO_R31((4*8)) + RESTORE_F24_TO_F31((14*8)) + MOVV (22*8)(R3), R1 + + ADDV $(23*8), R3 + + RET diff --git a/vendor/github.com/ebitengine/purego/internal/fakecgo/callbacks.go b/vendor/github.com/ebitengine/purego/internal/fakecgo/callbacks.go new file mode 100644 index 0000000..27d4c98 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/fakecgo/callbacks.go @@ -0,0 +1,93 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !cgo && (darwin || freebsd || linux || netbsd) + +package fakecgo + +import ( + _ "unsafe" +) + +// TODO: decide if we need _runtime_cgo_panic_internal + +//go:linkname x_cgo_init_trampoline x_cgo_init_trampoline +//go:linkname _cgo_init _cgo_init +var x_cgo_init_trampoline byte +var _cgo_init = &x_cgo_init_trampoline + +// Creates a new system thread without updating any Go state. +// +// This method is invoked during shared library loading to create a new OS +// thread to perform the runtime initialization. This method is similar to +// _cgo_sys_thread_start except that it doesn't update any Go state. + +//go:linkname x_cgo_thread_start_trampoline x_cgo_thread_start_trampoline +//go:linkname _cgo_thread_start _cgo_thread_start +var x_cgo_thread_start_trampoline byte +var _cgo_thread_start = &x_cgo_thread_start_trampoline + +// Notifies that the runtime has been initialized. +// +// We currently block at every CGO entry point (via _cgo_wait_runtime_init_done) +// to ensure that the runtime has been initialized before the CGO call is +// executed. This is necessary for shared libraries where we kickoff runtime +// initialization in a separate thread and return without waiting for this +// thread to complete the init. + +//go:linkname x_cgo_notify_runtime_init_done_trampoline x_cgo_notify_runtime_init_done_trampoline +//go:linkname _cgo_notify_runtime_init_done _cgo_notify_runtime_init_done +var x_cgo_notify_runtime_init_done_trampoline byte +var _cgo_notify_runtime_init_done = &x_cgo_notify_runtime_init_done_trampoline + +// Indicates whether a dummy thread key has been created or not. +// +// When calling go exported function from C, we register a destructor +// callback, for a dummy thread key, by using pthread_key_create. + +//go:linkname _cgo_pthread_key_created _cgo_pthread_key_created +var x_cgo_pthread_key_created uintptr +var _cgo_pthread_key_created = &x_cgo_pthread_key_created + +// Set the x_crosscall2_ptr C function pointer variable point to crosscall2. +// It's for the runtime package to call at init time. +func set_crosscall2() { + // nothing needs to be done here for fakecgo + // because it's possible to just call cgocallback directly +} + +//go:linkname _set_crosscall2 runtime.set_crosscall2 +var _set_crosscall2 = set_crosscall2 + +// Store the g into the thread-specific value. +// So that pthread_key_destructor will dropm when the thread is exiting. + +//go:linkname x_cgo_bindm_trampoline x_cgo_bindm_trampoline +//go:linkname _cgo_bindm _cgo_bindm +var x_cgo_bindm_trampoline byte +var _cgo_bindm = &x_cgo_bindm_trampoline + +// TODO: decide if we need x_cgo_set_context_function +// TODO: decide if we need _cgo_yield + +var ( + // In Go 1.20 the race detector was rewritten to pure Go + // on darwin. This means that when CGO_ENABLED=0 is set + // fakecgo is built with race detector code. This is not + // good since this code is pretending to be C. The go:norace + // pragma is not enough, since it only applies to the native + // ABIInternal function. The ABIO wrapper (which is necessary, + // since all references to text symbols from assembly will use it) + // does not inherit the go:norace pragma, so it will still be + // instrumented by the race detector. + // + // To circumvent this issue, using closure calls in the + // assembly, which forces the compiler to use the ABIInternal + // native implementation (which has go:norace) instead. + threadentry_call = threadentry + x_cgo_init_call = x_cgo_init + x_cgo_setenv_call = x_cgo_setenv + x_cgo_unsetenv_call = x_cgo_unsetenv + x_cgo_thread_start_call = x_cgo_thread_start +) diff --git a/vendor/github.com/ebitengine/purego/internal/fakecgo/doc.go b/vendor/github.com/ebitengine/purego/internal/fakecgo/doc.go new file mode 100644 index 0000000..e482c12 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/fakecgo/doc.go @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2022 The Ebitengine Authors + +//go:build !cgo && (darwin || freebsd || linux || netbsd) + +// Package fakecgo implements the Cgo runtime (runtime/cgo) entirely in Go. +// This allows code that calls into C to function properly when CGO_ENABLED=0. +// +// # Goals +// +// fakecgo attempts to replicate the same naming structure as in the runtime. +// For example, functions that have the prefix "gcc_*" are named "go_*". +// This makes it easier to port other GOOSs and GOARCHs as well as to keep +// it in sync with runtime/cgo. +// +// # Support +// +// Currently, fakecgo only supports macOS on amd64 & arm64. It also cannot +// be used with -buildmode=c-archive because that requires special initialization +// that fakecgo does not implement at the moment. +// +// # Usage +// +// Using fakecgo is easy just import _ "github.com/ebitengine/purego" and then +// set the environment variable CGO_ENABLED=0. +// The recommended usage for fakecgo is to prefer using runtime/cgo if possible +// but if cross-compiling or fast build times are important fakecgo is available. +// Purego will pick which ever Cgo runtime is available and prefer the one that +// comes with Go (runtime/cgo). +package fakecgo + +//go:generate go run gen.go diff --git a/vendor/github.com/ebitengine/purego/internal/fakecgo/freebsd.go b/vendor/github.com/ebitengine/purego/internal/fakecgo/freebsd.go new file mode 100644 index 0000000..bb73a70 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/fakecgo/freebsd.go @@ -0,0 +1,27 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build freebsd && !cgo + +package fakecgo + +import _ "unsafe" // for go:linkname + +// Supply environ and __progname, because we don't +// link against the standard FreeBSD crt0.o and the +// libc dynamic library needs them. + +// Note: when building with cross-compiling or CGO_ENABLED=0, add +// the following argument to `go` so that these symbols are defined by +// making fakecgo the Cgo. +// -gcflags="github.com/ebitengine/purego/internal/fakecgo=-std" + +//go:linkname _environ environ +//go:linkname _progname __progname + +//go:cgo_export_dynamic environ +//go:cgo_export_dynamic __progname + +var _environ uintptr +var _progname uintptr diff --git a/vendor/github.com/ebitengine/purego/internal/fakecgo/go_darwin_amd64.go b/vendor/github.com/ebitengine/purego/internal/fakecgo/go_darwin_amd64.go new file mode 100644 index 0000000..39f5ff1 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/fakecgo/go_darwin_amd64.go @@ -0,0 +1,73 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !cgo + +package fakecgo + +import "unsafe" + +//go:nosplit +//go:norace +func _cgo_sys_thread_start(ts *ThreadStart) { + var attr pthread_attr_t + var ign, oset sigset_t + var p pthread_t + var size size_t + var err int + + sigfillset(&ign) + pthread_sigmask(SIG_SETMASK, &ign, &oset) + + size = pthread_get_stacksize_np(pthread_self()) + pthread_attr_init(&attr) + pthread_attr_setstacksize(&attr, size) + // Leave stacklo=0 and set stackhi=size; mstart will do the rest. + ts.g.stackhi = uintptr(size) + + err = _cgo_try_pthread_create(&p, &attr, unsafe.Pointer(threadentry_trampolineABI0), ts) + + pthread_sigmask(SIG_SETMASK, &oset, nil) + + if err != 0 { + print("fakecgo: pthread_create failed: ") + println(err) + abort() + } +} + +// threadentry_trampolineABI0 maps the C ABI to Go ABI then calls the Go function +// +//go:linkname x_threadentry_trampoline threadentry_trampoline +var x_threadentry_trampoline byte +var threadentry_trampolineABI0 = &x_threadentry_trampoline + +//go:nosplit +//go:norace +func threadentry(v unsafe.Pointer) unsafe.Pointer { + ts := *(*ThreadStart)(v) + free(v) + + setg_trampoline(setg_func, uintptr(unsafe.Pointer(ts.g))) + + // faking funcs in go is a bit a... involved - but the following works :) + fn := uintptr(unsafe.Pointer(&ts.fn)) + (*(*func())(unsafe.Pointer(&fn)))() + + return nil +} + +// here we will store a pointer to the provided setg func +var setg_func uintptr + +//go:nosplit +//go:norace +func x_cgo_init(g *G, setg uintptr) { + var size size_t + + setg_func = setg + + size = pthread_get_stacksize_np(pthread_self()) + g.stacklo = uintptr(unsafe.Add(unsafe.Pointer(&size), -size+4096)) +} diff --git a/vendor/github.com/ebitengine/purego/internal/fakecgo/go_darwin_arm64.go b/vendor/github.com/ebitengine/purego/internal/fakecgo/go_darwin_arm64.go new file mode 100644 index 0000000..d0868f0 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/fakecgo/go_darwin_arm64.go @@ -0,0 +1,88 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !cgo + +package fakecgo + +import "unsafe" + +//go:nosplit +//go:norace +func _cgo_sys_thread_start(ts *ThreadStart) { + var attr pthread_attr_t + var ign, oset sigset_t + var p pthread_t + var size size_t + var err int + + sigfillset(&ign) + pthread_sigmask(SIG_SETMASK, &ign, &oset) + + size = pthread_get_stacksize_np(pthread_self()) + pthread_attr_init(&attr) + pthread_attr_setstacksize(&attr, size) + // Leave stacklo=0 and set stackhi=size; mstart will do the rest. + ts.g.stackhi = uintptr(size) + + err = _cgo_try_pthread_create(&p, &attr, unsafe.Pointer(threadentry_trampolineABI0), ts) + + pthread_sigmask(SIG_SETMASK, &oset, nil) + + if err != 0 { + print("fakecgo: pthread_create failed: ") + println(err) + abort() + } +} + +// threadentry_trampolineABI0 maps the C ABI to Go ABI then calls the Go function +// +//go:linkname x_threadentry_trampoline threadentry_trampoline +var x_threadentry_trampoline byte +var threadentry_trampolineABI0 = &x_threadentry_trampoline + +//go:nosplit +//go:norace +func threadentry(v unsafe.Pointer) unsafe.Pointer { + ts := *(*ThreadStart)(v) + free(v) + + // TODO: support ios + //#if TARGET_OS_IPHONE + // darwin_arm_init_thread_exception_port(); + //#endif + setg_trampoline(setg_func, uintptr(unsafe.Pointer(ts.g))) + + // faking funcs in go is a bit a... involved - but the following works :) + fn := uintptr(unsafe.Pointer(&ts.fn)) + (*(*func())(unsafe.Pointer(&fn)))() + + return nil +} + +// here we will store a pointer to the provided setg func +var setg_func uintptr + +// x_cgo_init(G *g, void (*setg)(void*)) (runtime/cgo/gcc_linux_amd64.c) +// This get's called during startup, adjusts stacklo, and provides a pointer to setg_gcc for us +// Additionally, if we set _cgo_init to non-null, go won't do it's own TLS setup +// This function can't be go:systemstack since go is not in a state where the systemcheck would work. +// +//go:nosplit +//go:norace +func x_cgo_init(g *G, setg uintptr) { + var size size_t + + setg_func = setg + size = pthread_get_stacksize_np(pthread_self()) + g.stacklo = uintptr(unsafe.Add(unsafe.Pointer(&size), -size+4096)) + + //TODO: support ios + //#if TARGET_OS_IPHONE + // darwin_arm_init_mach_exception_handler(); + // darwin_arm_init_thread_exception_port(); + // init_working_dir(); + //#endif +} diff --git a/vendor/github.com/ebitengine/purego/internal/fakecgo/go_freebsd_amd64.go b/vendor/github.com/ebitengine/purego/internal/fakecgo/go_freebsd_amd64.go new file mode 100644 index 0000000..c9ff715 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/fakecgo/go_freebsd_amd64.go @@ -0,0 +1,95 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !cgo + +package fakecgo + +import "unsafe" + +//go:nosplit +func _cgo_sys_thread_start(ts *ThreadStart) { + var attr pthread_attr_t + var ign, oset sigset_t + var p pthread_t + var size size_t + var err int + + //fprintf(stderr, "runtime/cgo: _cgo_sys_thread_start: fn=%p, g=%p\n", ts->fn, ts->g); // debug + sigfillset(&ign) + pthread_sigmask(SIG_SETMASK, &ign, &oset) + + pthread_attr_init(&attr) + pthread_attr_getstacksize(&attr, &size) + // Leave stacklo=0 and set stackhi=size; mstart will do the rest. + ts.g.stackhi = uintptr(size) + + err = _cgo_try_pthread_create(&p, &attr, unsafe.Pointer(threadentry_trampolineABI0), ts) + + pthread_sigmask(SIG_SETMASK, &oset, nil) + + if err != 0 { + print("fakecgo: pthread_create failed: ") + println(err) + abort() + } +} + +// threadentry_trampolineABI0 maps the C ABI to Go ABI then calls the Go function +// +//go:linkname x_threadentry_trampoline threadentry_trampoline +var x_threadentry_trampoline byte +var threadentry_trampolineABI0 = &x_threadentry_trampoline + +//go:nosplit +func threadentry(v unsafe.Pointer) unsafe.Pointer { + ts := *(*ThreadStart)(v) + free(v) + + setg_trampoline(setg_func, uintptr(unsafe.Pointer(ts.g))) + + // faking funcs in go is a bit a... involved - but the following works :) + fn := uintptr(unsafe.Pointer(&ts.fn)) + (*(*func())(unsafe.Pointer(&fn)))() + + return nil +} + +// here we will store a pointer to the provided setg func +var setg_func uintptr + +//go:nosplit +func x_cgo_init(g *G, setg uintptr) { + var size size_t + var attr *pthread_attr_t + + /* The memory sanitizer distributed with versions of clang + before 3.8 has a bug: if you call mmap before malloc, mmap + may return an address that is later overwritten by the msan + library. Avoid this problem by forcing a call to malloc + here, before we ever call malloc. + + This is only required for the memory sanitizer, so it's + unfortunate that we always run it. It should be possible + to remove this when we no longer care about versions of + clang before 3.8. The test for this is + misc/cgo/testsanitizers. + + GCC works hard to eliminate a seemingly unnecessary call to + malloc, so we actually use the memory we allocate. */ + + setg_func = setg + attr = (*pthread_attr_t)(malloc(unsafe.Sizeof(*attr))) + if attr == nil { + println("fakecgo: malloc failed") + abort() + } + pthread_attr_init(attr) + pthread_attr_getstacksize(attr, &size) + // runtime/cgo uses __builtin_frame_address(0) instead of `uintptr(unsafe.Pointer(&size))` + // but this should be OK since we are taking the address of the first variable in this function. + g.stacklo = uintptr(unsafe.Pointer(&size)) - uintptr(size) + 4096 + pthread_attr_destroy(attr) + free(unsafe.Pointer(attr)) +} diff --git a/vendor/github.com/ebitengine/purego/internal/fakecgo/go_freebsd_arm64.go b/vendor/github.com/ebitengine/purego/internal/fakecgo/go_freebsd_arm64.go new file mode 100644 index 0000000..e3a060b --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/fakecgo/go_freebsd_arm64.go @@ -0,0 +1,98 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !cgo + +package fakecgo + +import "unsafe" + +//go:nosplit +func _cgo_sys_thread_start(ts *ThreadStart) { + var attr pthread_attr_t + var ign, oset sigset_t + var p pthread_t + var size size_t + var err int + + // fprintf(stderr, "runtime/cgo: _cgo_sys_thread_start: fn=%p, g=%p\n", ts->fn, ts->g); // debug + sigfillset(&ign) + pthread_sigmask(SIG_SETMASK, &ign, &oset) + + pthread_attr_init(&attr) + pthread_attr_getstacksize(&attr, &size) + // Leave stacklo=0 and set stackhi=size; mstart will do the rest. + ts.g.stackhi = uintptr(size) + + err = _cgo_try_pthread_create(&p, &attr, unsafe.Pointer(threadentry_trampolineABI0), ts) + + pthread_sigmask(SIG_SETMASK, &oset, nil) + + if err != 0 { + print("fakecgo: pthread_create failed: ") + println(err) + abort() + } +} + +// threadentry_trampolineABI0 maps the C ABI to Go ABI then calls the Go function +// +//go:linkname x_threadentry_trampoline threadentry_trampoline +var x_threadentry_trampoline byte +var threadentry_trampolineABI0 = &x_threadentry_trampoline + +//go:nosplit +func threadentry(v unsafe.Pointer) unsafe.Pointer { + ts := *(*ThreadStart)(v) + free(v) + + setg_trampoline(setg_func, uintptr(unsafe.Pointer(ts.g))) + + // faking funcs in go is a bit a... involved - but the following works :) + fn := uintptr(unsafe.Pointer(&ts.fn)) + (*(*func())(unsafe.Pointer(&fn)))() + + return nil +} + +// here we will store a pointer to the provided setg func +var setg_func uintptr + +// x_cgo_init(G *g, void (*setg)(void*)) (runtime/cgo/gcc_linux_amd64.c) +// This get's called during startup, adjusts stacklo, and provides a pointer to setg_gcc for us +// Additionally, if we set _cgo_init to non-null, go won't do it's own TLS setup +// This function can't be go:systemstack since go is not in a state where the systemcheck would work. +// +//go:nosplit +func x_cgo_init(g *G, setg uintptr) { + var size size_t + var attr *pthread_attr_t + + /* The memory sanitizer distributed with versions of clang + before 3.8 has a bug: if you call mmap before malloc, mmap + may return an address that is later overwritten by the msan + library. Avoid this problem by forcing a call to malloc + here, before we ever call malloc. + + This is only required for the memory sanitizer, so it's + unfortunate that we always run it. It should be possible + to remove this when we no longer care about versions of + clang before 3.8. The test for this is + misc/cgo/testsanitizers. + + GCC works hard to eliminate a seemingly unnecessary call to + malloc, so we actually use the memory we allocate. */ + + setg_func = setg + attr = (*pthread_attr_t)(malloc(unsafe.Sizeof(*attr))) + if attr == nil { + println("fakecgo: malloc failed") + abort() + } + pthread_attr_init(attr) + pthread_attr_getstacksize(attr, &size) + g.stacklo = uintptr(unsafe.Pointer(&size)) - uintptr(size) + 4096 + pthread_attr_destroy(attr) + free(unsafe.Pointer(attr)) +} diff --git a/vendor/github.com/ebitengine/purego/internal/fakecgo/go_libinit.go b/vendor/github.com/ebitengine/purego/internal/fakecgo/go_libinit.go new file mode 100644 index 0000000..0c46306 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/fakecgo/go_libinit.go @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2022 The Ebitengine Authors + +//go:build !cgo && (darwin || freebsd || linux || netbsd) + +package fakecgo + +import ( + "syscall" + "unsafe" +) + +var ( + pthread_g pthread_key_t + + runtime_init_cond = PTHREAD_COND_INITIALIZER + runtime_init_mu = PTHREAD_MUTEX_INITIALIZER + runtime_init_done int +) + +//go:nosplit +//go:norace +func x_cgo_notify_runtime_init_done() { + pthread_mutex_lock(&runtime_init_mu) + runtime_init_done = 1 + pthread_cond_broadcast(&runtime_init_cond) + pthread_mutex_unlock(&runtime_init_mu) +} + +// Store the g into a thread-specific value associated with the pthread key pthread_g. +// And pthread_key_destructor will dropm when the thread is exiting. +// +//go:norace +func x_cgo_bindm(g unsafe.Pointer) { + // We assume this will always succeed, otherwise, there might be extra M leaking, + // when a C thread exits after a cgo call. + // We only invoke this function once per thread in runtime.needAndBindM, + // and the next calls just reuse the bound m. + pthread_setspecific(pthread_g, g) +} + +// _cgo_try_pthread_create retries pthread_create if it fails with +// EAGAIN. +// +//go:nosplit +//go:norace +func _cgo_try_pthread_create(thread *pthread_t, attr *pthread_attr_t, pfn unsafe.Pointer, arg *ThreadStart) int { + var ts syscall.Timespec + // tries needs to be the same type as syscall.Timespec.Nsec + // but the fields are int32 on 32bit and int64 on 64bit. + // tries is assigned to syscall.Timespec.Nsec in order to match its type. + tries := ts.Nsec + var err int + + for tries = 0; tries < 20; tries++ { + // inlined this call because it ran out of stack when inlining was disabled + err = int(call5(pthread_createABI0, uintptr(unsafe.Pointer(thread)), uintptr(unsafe.Pointer(attr)), uintptr(pfn), uintptr(unsafe.Pointer(arg)), 0)) + if err == 0 { + // inlined this call because it ran out of stack when inlining was disabled + call5(pthread_detachABI0, uintptr(*thread), 0, 0, 0, 0) + return 0 + } + if err != int(syscall.EAGAIN) { + return err + } + ts.Sec = 0 + ts.Nsec = (tries + 1) * 1000 * 1000 // Milliseconds. + // inlined this call because it ran out of stack when inlining was disabled + call5(nanosleepABI0, uintptr(unsafe.Pointer(&ts)), 0, 0, 0, 0) + } + return int(syscall.EAGAIN) +} diff --git a/vendor/github.com/ebitengine/purego/internal/fakecgo/go_linux_amd64.go b/vendor/github.com/ebitengine/purego/internal/fakecgo/go_linux_amd64.go new file mode 100644 index 0000000..c9ff715 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/fakecgo/go_linux_amd64.go @@ -0,0 +1,95 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !cgo + +package fakecgo + +import "unsafe" + +//go:nosplit +func _cgo_sys_thread_start(ts *ThreadStart) { + var attr pthread_attr_t + var ign, oset sigset_t + var p pthread_t + var size size_t + var err int + + //fprintf(stderr, "runtime/cgo: _cgo_sys_thread_start: fn=%p, g=%p\n", ts->fn, ts->g); // debug + sigfillset(&ign) + pthread_sigmask(SIG_SETMASK, &ign, &oset) + + pthread_attr_init(&attr) + pthread_attr_getstacksize(&attr, &size) + // Leave stacklo=0 and set stackhi=size; mstart will do the rest. + ts.g.stackhi = uintptr(size) + + err = _cgo_try_pthread_create(&p, &attr, unsafe.Pointer(threadentry_trampolineABI0), ts) + + pthread_sigmask(SIG_SETMASK, &oset, nil) + + if err != 0 { + print("fakecgo: pthread_create failed: ") + println(err) + abort() + } +} + +// threadentry_trampolineABI0 maps the C ABI to Go ABI then calls the Go function +// +//go:linkname x_threadentry_trampoline threadentry_trampoline +var x_threadentry_trampoline byte +var threadentry_trampolineABI0 = &x_threadentry_trampoline + +//go:nosplit +func threadentry(v unsafe.Pointer) unsafe.Pointer { + ts := *(*ThreadStart)(v) + free(v) + + setg_trampoline(setg_func, uintptr(unsafe.Pointer(ts.g))) + + // faking funcs in go is a bit a... involved - but the following works :) + fn := uintptr(unsafe.Pointer(&ts.fn)) + (*(*func())(unsafe.Pointer(&fn)))() + + return nil +} + +// here we will store a pointer to the provided setg func +var setg_func uintptr + +//go:nosplit +func x_cgo_init(g *G, setg uintptr) { + var size size_t + var attr *pthread_attr_t + + /* The memory sanitizer distributed with versions of clang + before 3.8 has a bug: if you call mmap before malloc, mmap + may return an address that is later overwritten by the msan + library. Avoid this problem by forcing a call to malloc + here, before we ever call malloc. + + This is only required for the memory sanitizer, so it's + unfortunate that we always run it. It should be possible + to remove this when we no longer care about versions of + clang before 3.8. The test for this is + misc/cgo/testsanitizers. + + GCC works hard to eliminate a seemingly unnecessary call to + malloc, so we actually use the memory we allocate. */ + + setg_func = setg + attr = (*pthread_attr_t)(malloc(unsafe.Sizeof(*attr))) + if attr == nil { + println("fakecgo: malloc failed") + abort() + } + pthread_attr_init(attr) + pthread_attr_getstacksize(attr, &size) + // runtime/cgo uses __builtin_frame_address(0) instead of `uintptr(unsafe.Pointer(&size))` + // but this should be OK since we are taking the address of the first variable in this function. + g.stacklo = uintptr(unsafe.Pointer(&size)) - uintptr(size) + 4096 + pthread_attr_destroy(attr) + free(unsafe.Pointer(attr)) +} diff --git a/vendor/github.com/ebitengine/purego/internal/fakecgo/go_linux_arm64.go b/vendor/github.com/ebitengine/purego/internal/fakecgo/go_linux_arm64.go new file mode 100644 index 0000000..a3b1cca --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/fakecgo/go_linux_arm64.go @@ -0,0 +1,98 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !cgo + +package fakecgo + +import "unsafe" + +//go:nosplit +func _cgo_sys_thread_start(ts *ThreadStart) { + var attr pthread_attr_t + var ign, oset sigset_t + var p pthread_t + var size size_t + var err int + + //fprintf(stderr, "runtime/cgo: _cgo_sys_thread_start: fn=%p, g=%p\n", ts->fn, ts->g); // debug + sigfillset(&ign) + pthread_sigmask(SIG_SETMASK, &ign, &oset) + + pthread_attr_init(&attr) + pthread_attr_getstacksize(&attr, &size) + // Leave stacklo=0 and set stackhi=size; mstart will do the rest. + ts.g.stackhi = uintptr(size) + + err = _cgo_try_pthread_create(&p, &attr, unsafe.Pointer(threadentry_trampolineABI0), ts) + + pthread_sigmask(SIG_SETMASK, &oset, nil) + + if err != 0 { + print("fakecgo: pthread_create failed: ") + println(err) + abort() + } +} + +// threadentry_trampolineABI0 maps the C ABI to Go ABI then calls the Go function +// +//go:linkname x_threadentry_trampoline threadentry_trampoline +var x_threadentry_trampoline byte +var threadentry_trampolineABI0 = &x_threadentry_trampoline + +//go:nosplit +func threadentry(v unsafe.Pointer) unsafe.Pointer { + ts := *(*ThreadStart)(v) + free(v) + + setg_trampoline(setg_func, uintptr(unsafe.Pointer(ts.g))) + + // faking funcs in go is a bit a... involved - but the following works :) + fn := uintptr(unsafe.Pointer(&ts.fn)) + (*(*func())(unsafe.Pointer(&fn)))() + + return nil +} + +// here we will store a pointer to the provided setg func +var setg_func uintptr + +// x_cgo_init(G *g, void (*setg)(void*)) (runtime/cgo/gcc_linux_amd64.c) +// This get's called during startup, adjusts stacklo, and provides a pointer to setg_gcc for us +// Additionally, if we set _cgo_init to non-null, go won't do it's own TLS setup +// This function can't be go:systemstack since go is not in a state where the systemcheck would work. +// +//go:nosplit +func x_cgo_init(g *G, setg uintptr) { + var size size_t + var attr *pthread_attr_t + + /* The memory sanitizer distributed with versions of clang + before 3.8 has a bug: if you call mmap before malloc, mmap + may return an address that is later overwritten by the msan + library. Avoid this problem by forcing a call to malloc + here, before we ever call malloc. + + This is only required for the memory sanitizer, so it's + unfortunate that we always run it. It should be possible + to remove this when we no longer care about versions of + clang before 3.8. The test for this is + misc/cgo/testsanitizers. + + GCC works hard to eliminate a seemingly unnecessary call to + malloc, so we actually use the memory we allocate. */ + + setg_func = setg + attr = (*pthread_attr_t)(malloc(unsafe.Sizeof(*attr))) + if attr == nil { + println("fakecgo: malloc failed") + abort() + } + pthread_attr_init(attr) + pthread_attr_getstacksize(attr, &size) + g.stacklo = uintptr(unsafe.Pointer(&size)) - uintptr(size) + 4096 + pthread_attr_destroy(attr) + free(unsafe.Pointer(attr)) +} diff --git a/vendor/github.com/ebitengine/purego/internal/fakecgo/go_linux_loong64.go b/vendor/github.com/ebitengine/purego/internal/fakecgo/go_linux_loong64.go new file mode 100644 index 0000000..6529391 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/fakecgo/go_linux_loong64.go @@ -0,0 +1,92 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !cgo + +package fakecgo + +import "unsafe" + +//go:nosplit +func _cgo_sys_thread_start(ts *ThreadStart) { + var attr pthread_attr_t + var ign, oset sigset_t + var p pthread_t + var size size_t + var err int + + sigfillset(&ign) + pthread_sigmask(SIG_SETMASK, &ign, &oset) + + pthread_attr_init(&attr) + pthread_attr_getstacksize(&attr, &size) + // Leave stacklo=0 and set stackhi=size; mstart will do the rest. + ts.g.stackhi = uintptr(size) + + err = _cgo_try_pthread_create(&p, &attr, unsafe.Pointer(threadentry_trampolineABI0), ts) + + pthread_sigmask(SIG_SETMASK, &oset, nil) + + if err != 0 { + print("fakecgo: pthread_create failed: ") + println(err) + abort() + } +} + +// threadentry_trampolineABI0 maps the C ABI to Go ABI then calls the Go function +// +//go:linkname x_threadentry_trampoline threadentry_trampoline +var x_threadentry_trampoline byte +var threadentry_trampolineABI0 = &x_threadentry_trampoline + +//go:nosplit +func threadentry(v unsafe.Pointer) unsafe.Pointer { + ts := *(*ThreadStart)(v) + free(v) + + setg_trampoline(setg_func, uintptr(unsafe.Pointer(ts.g))) + + // faking funcs in go is a bit a... involved - but the following works :) + fn := uintptr(unsafe.Pointer(&ts.fn)) + (*(*func())(unsafe.Pointer(&fn)))() + + return nil +} + +// here we will store a pointer to the provided setg func +var setg_func uintptr + +//go:nosplit +func x_cgo_init(g *G, setg uintptr) { + var size size_t + var attr *pthread_attr_t + + /* The memory sanitizer distributed with versions of clang + before 3.8 has a bug: if you call mmap before malloc, mmap + may return an address that is later overwritten by the msan + library. Avoid this problem by forcing a call to malloc + here, before we ever call malloc. + + This is only required for the memory sanitizer, so it's + unfortunate that we always run it. It should be possible + to remove this when we no longer care about versions of + clang before 3.8. The test for this is + misc/cgo/testsanitizers. + + GCC works hard to eliminate a seemingly unnecessary call to + malloc, so we actually use the memory we allocate. */ + + setg_func = setg + attr = (*pthread_attr_t)(malloc(unsafe.Sizeof(*attr))) + if attr == nil { + println("fakecgo: malloc failed") + abort() + } + pthread_attr_init(attr) + pthread_attr_getstacksize(attr, &size) + g.stacklo = uintptr(unsafe.Pointer(&size)) - uintptr(size) + 4096 + pthread_attr_destroy(attr) + free(unsafe.Pointer(attr)) +} diff --git a/vendor/github.com/ebitengine/purego/internal/fakecgo/go_netbsd.go b/vendor/github.com/ebitengine/purego/internal/fakecgo/go_netbsd.go new file mode 100644 index 0000000..935a334 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/fakecgo/go_netbsd.go @@ -0,0 +1,106 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !cgo && (amd64 || arm64) + +package fakecgo + +import "unsafe" + +//go:nosplit +func _cgo_sys_thread_start(ts *ThreadStart) { + var attr pthread_attr_t + var ign, oset sigset_t + var p pthread_t + var size size_t + var err int + + // fprintf(stderr, "runtime/cgo: _cgo_sys_thread_start: fn=%p, g=%p\n", ts->fn, ts->g); // debug + sigfillset(&ign) + pthread_sigmask(SIG_SETMASK, &ign, &oset) + + pthread_attr_init(&attr) + pthread_attr_getstacksize(&attr, &size) + // Leave stacklo=0 and set stackhi=size; mstart will do the rest. + ts.g.stackhi = uintptr(size) + + err = _cgo_try_pthread_create(&p, &attr, unsafe.Pointer(threadentry_trampolineABI0), ts) + + pthread_sigmask(SIG_SETMASK, &oset, nil) + + if err != 0 { + print("fakecgo: pthread_create failed: ") + println(err) + abort() + } +} + +// threadentry_trampolineABI0 maps the C ABI to Go ABI then calls the Go function +// +//go:linkname x_threadentry_trampoline threadentry_trampoline +var x_threadentry_trampoline byte +var threadentry_trampolineABI0 = &x_threadentry_trampoline + +//go:nosplit +func threadentry(v unsafe.Pointer) unsafe.Pointer { + var ss stack_t + ts := *(*ThreadStart)(v) + free(v) + + // On NetBSD, a new thread inherits the signal stack of the + // creating thread. That confuses minit, so we remove that + // signal stack here before calling the regular mstart. It's + // a bit baroque to remove a signal stack here only to add one + // in minit, but it's a simple change that keeps NetBSD + // working like other OS's. At this point all signals are + // blocked, so there is no race. + ss.ss_flags = SS_DISABLE + sigaltstack(&ss, nil) + + setg_trampoline(setg_func, uintptr(unsafe.Pointer(ts.g))) + + // faking funcs in go is a bit a... involved - but the following works :) + fn := uintptr(unsafe.Pointer(&ts.fn)) + (*(*func())(unsafe.Pointer(&fn)))() + + return nil +} + +// here we will store a pointer to the provided setg func +var setg_func uintptr + +//go:nosplit +func x_cgo_init(g *G, setg uintptr) { + var size size_t + var attr *pthread_attr_t + + /* The memory sanitizer distributed with versions of clang + before 3.8 has a bug: if you call mmap before malloc, mmap + may return an address that is later overwritten by the msan + library. Avoid this problem by forcing a call to malloc + here, before we ever call malloc. + + This is only required for the memory sanitizer, so it's + unfortunate that we always run it. It should be possible + to remove this when we no longer care about versions of + clang before 3.8. The test for this is + misc/cgo/testsanitizers. + + GCC works hard to eliminate a seemingly unnecessary call to + malloc, so we actually use the memory we allocate. */ + + setg_func = setg + attr = (*pthread_attr_t)(malloc(unsafe.Sizeof(*attr))) + if attr == nil { + println("fakecgo: malloc failed") + abort() + } + pthread_attr_init(attr) + pthread_attr_getstacksize(attr, &size) + // runtime/cgo uses __builtin_frame_address(0) instead of `uintptr(unsafe.Pointer(&size))` + // but this should be OK since we are taking the address of the first variable in this function. + g.stacklo = uintptr(unsafe.Pointer(&size)) - uintptr(size) + 4096 + pthread_attr_destroy(attr) + free(unsafe.Pointer(attr)) +} diff --git a/vendor/github.com/ebitengine/purego/internal/fakecgo/go_setenv.go b/vendor/github.com/ebitengine/purego/internal/fakecgo/go_setenv.go new file mode 100644 index 0000000..dfc6629 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/fakecgo/go_setenv.go @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2022 The Ebitengine Authors + +//go:build !cgo && (darwin || freebsd || linux || netbsd) + +package fakecgo + +//go:nosplit +//go:norace +func x_cgo_setenv(arg *[2]*byte) { + setenv(arg[0], arg[1], 1) +} + +//go:nosplit +//go:norace +func x_cgo_unsetenv(arg *[1]*byte) { + unsetenv(arg[0]) +} diff --git a/vendor/github.com/ebitengine/purego/internal/fakecgo/go_util.go b/vendor/github.com/ebitengine/purego/internal/fakecgo/go_util.go new file mode 100644 index 0000000..771cb52 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/fakecgo/go_util.go @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2022 The Ebitengine Authors + +//go:build !cgo && (darwin || freebsd || linux || netbsd) + +package fakecgo + +import "unsafe" + +// _cgo_thread_start is split into three parts in cgo since only one part is system dependent (keep it here for easier handling) + +// _cgo_thread_start(ThreadStart *arg) (runtime/cgo/gcc_util.c) +// This get's called instead of the go code for creating new threads +// -> pthread_* stuff is used, so threads are setup correctly for C +// If this is missing, TLS is only setup correctly on thread 1! +// This function should be go:systemstack instead of go:nosplit (but that requires runtime) +// +//go:nosplit +//go:norace +func x_cgo_thread_start(arg *ThreadStart) { + var ts *ThreadStart + // Make our own copy that can persist after we return. + // _cgo_tsan_acquire(); + ts = (*ThreadStart)(malloc(unsafe.Sizeof(*ts))) + // _cgo_tsan_release(); + if ts == nil { + println("fakecgo: out of memory in thread_start") + abort() + } + // *ts = *arg would cause a writebarrier so copy using slices + s1 := unsafe.Slice((*uintptr)(unsafe.Pointer(ts)), unsafe.Sizeof(*ts)/8) + s2 := unsafe.Slice((*uintptr)(unsafe.Pointer(arg)), unsafe.Sizeof(*arg)/8) + for i := range s2 { + s1[i] = s2[i] + } + _cgo_sys_thread_start(ts) // OS-dependent half +} diff --git a/vendor/github.com/ebitengine/purego/internal/fakecgo/iscgo.go b/vendor/github.com/ebitengine/purego/internal/fakecgo/iscgo.go new file mode 100644 index 0000000..12e5214 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/fakecgo/iscgo.go @@ -0,0 +1,19 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !cgo && (darwin || freebsd || linux || netbsd) + +// The runtime package contains an uninitialized definition +// for runtime·iscgo. Override it to tell the runtime we're here. +// There are various function pointers that should be set too, +// but those depend on dynamic linker magic to get initialized +// correctly, and sometimes they break. This variable is a +// backup: it depends only on old C style static linking rules. + +package fakecgo + +import _ "unsafe" // for go:linkname + +//go:linkname _iscgo runtime.iscgo +var _iscgo bool = true diff --git a/vendor/github.com/ebitengine/purego/internal/fakecgo/libcgo.go b/vendor/github.com/ebitengine/purego/internal/fakecgo/libcgo.go new file mode 100644 index 0000000..94fd8be --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/fakecgo/libcgo.go @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2022 The Ebitengine Authors + +//go:build !cgo && (darwin || freebsd || linux || netbsd) + +package fakecgo + +type ( + size_t uintptr + // Sources: + // Darwin (32 bytes) - https://github.com/apple/darwin-xnu/blob/2ff845c2e033bd0ff64b5b6aa6063a1f8f65aa32/bsd/sys/_types.h#L74 + // FreeBSD (32 bytes) - https://github.com/DoctorWkt/xv6-freebsd/blob/d2a294c2a984baed27676068b15ed9a29b06ab6f/include/signal.h#L98C9-L98C21 + // Linux (128 bytes) - https://github.com/torvalds/linux/blob/ab75170520d4964f3acf8bb1f91d34cbc650688e/arch/x86/include/asm/signal.h#L25 + sigset_t [128]byte + pthread_attr_t [64]byte + pthread_t int + pthread_key_t uint64 +) + +// for pthread_sigmask: + +type sighow int32 + +const ( + SIG_BLOCK sighow = 0 + SIG_UNBLOCK sighow = 1 + SIG_SETMASK sighow = 2 +) + +type G struct { + stacklo uintptr + stackhi uintptr +} + +type ThreadStart struct { + g *G + tls *uintptr + fn uintptr +} diff --git a/vendor/github.com/ebitengine/purego/internal/fakecgo/libcgo_darwin.go b/vendor/github.com/ebitengine/purego/internal/fakecgo/libcgo_darwin.go new file mode 100644 index 0000000..ecdcb2e --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/fakecgo/libcgo_darwin.go @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2022 The Ebitengine Authors + +//go:build !cgo + +package fakecgo + +type ( + pthread_mutex_t struct { + sig int64 + opaque [56]byte + } + pthread_cond_t struct { + sig int64 + opaque [40]byte + } +) + +var ( + PTHREAD_COND_INITIALIZER = pthread_cond_t{sig: 0x3CB0B1BB} + PTHREAD_MUTEX_INITIALIZER = pthread_mutex_t{sig: 0x32AAABA7} +) + +type stack_t struct { + /* not implemented */ +} diff --git a/vendor/github.com/ebitengine/purego/internal/fakecgo/libcgo_freebsd.go b/vendor/github.com/ebitengine/purego/internal/fakecgo/libcgo_freebsd.go new file mode 100644 index 0000000..4bfb70c --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/fakecgo/libcgo_freebsd.go @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2022 The Ebitengine Authors + +//go:build !cgo + +package fakecgo + +type ( + pthread_cond_t uintptr + pthread_mutex_t uintptr +) + +var ( + PTHREAD_COND_INITIALIZER = pthread_cond_t(0) + PTHREAD_MUTEX_INITIALIZER = pthread_mutex_t(0) +) + +type stack_t struct { + /* not implemented */ +} diff --git a/vendor/github.com/ebitengine/purego/internal/fakecgo/libcgo_linux.go b/vendor/github.com/ebitengine/purego/internal/fakecgo/libcgo_linux.go new file mode 100644 index 0000000..b08a44a --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/fakecgo/libcgo_linux.go @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2022 The Ebitengine Authors + +//go:build !cgo + +package fakecgo + +type ( + pthread_cond_t [48]byte + pthread_mutex_t [48]byte +) + +var ( + PTHREAD_COND_INITIALIZER = pthread_cond_t{} + PTHREAD_MUTEX_INITIALIZER = pthread_mutex_t{} +) + +type stack_t struct { + /* not implemented */ +} diff --git a/vendor/github.com/ebitengine/purego/internal/fakecgo/libcgo_netbsd.go b/vendor/github.com/ebitengine/purego/internal/fakecgo/libcgo_netbsd.go new file mode 100644 index 0000000..650f695 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/fakecgo/libcgo_netbsd.go @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 The Ebitengine Authors + +//go:build !cgo + +package fakecgo + +type ( + pthread_cond_t uintptr + pthread_mutex_t uintptr +) + +var ( + PTHREAD_COND_INITIALIZER = pthread_cond_t(0) + PTHREAD_MUTEX_INITIALIZER = pthread_mutex_t(0) +) + +// Source: https://github.com/NetBSD/src/blob/613e27c65223fd2283b6ed679da1197e12f50e27/sys/compat/linux/arch/m68k/linux_signal.h#L133 +type stack_t struct { + ss_sp uintptr + ss_flags int32 + ss_size uintptr +} + +// Source: https://github.com/NetBSD/src/blob/613e27c65223fd2283b6ed679da1197e12f50e27/sys/sys/signal.h#L261 +const SS_DISABLE = 0x004 diff --git a/vendor/github.com/ebitengine/purego/internal/fakecgo/netbsd.go b/vendor/github.com/ebitengine/purego/internal/fakecgo/netbsd.go new file mode 100644 index 0000000..2d49981 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/fakecgo/netbsd.go @@ -0,0 +1,23 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build netbsd + +package fakecgo + +import _ "unsafe" // for go:linkname + +// Supply environ and __progname, because we don't +// link against the standard NetBSD crt0.o and the +// libc dynamic library needs them. + +//go:linkname _environ environ +//go:linkname _progname __progname +//go:linkname ___ps_strings __ps_strings + +var ( + _environ uintptr + _progname uintptr + ___ps_strings uintptr +) diff --git a/vendor/github.com/ebitengine/purego/internal/fakecgo/setenv.go b/vendor/github.com/ebitengine/purego/internal/fakecgo/setenv.go new file mode 100644 index 0000000..82308b8 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/fakecgo/setenv.go @@ -0,0 +1,19 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !cgo && (darwin || freebsd || linux || netbsd) + +package fakecgo + +import _ "unsafe" // for go:linkname + +//go:linkname x_cgo_setenv_trampoline x_cgo_setenv_trampoline +//go:linkname _cgo_setenv runtime._cgo_setenv +var x_cgo_setenv_trampoline byte +var _cgo_setenv = &x_cgo_setenv_trampoline + +//go:linkname x_cgo_unsetenv_trampoline x_cgo_unsetenv_trampoline +//go:linkname _cgo_unsetenv runtime._cgo_unsetenv +var x_cgo_unsetenv_trampoline byte +var _cgo_unsetenv = &x_cgo_unsetenv_trampoline diff --git a/vendor/github.com/ebitengine/purego/internal/fakecgo/symbols.go b/vendor/github.com/ebitengine/purego/internal/fakecgo/symbols.go new file mode 100644 index 0000000..135f6d2 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/fakecgo/symbols.go @@ -0,0 +1,231 @@ +// Code generated by 'go generate' with gen.go. DO NOT EDIT. + +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2022 The Ebitengine Authors + +//go:build !cgo && (darwin || freebsd || linux || netbsd) + +package fakecgo + +import ( + "syscall" + "unsafe" +) + +// setg_trampoline calls setg with the G provided +func setg_trampoline(setg uintptr, G uintptr) + +// call5 takes fn the C function and 5 arguments and calls the function with those arguments +func call5(fn, a1, a2, a3, a4, a5 uintptr) uintptr + +//go:nosplit +//go:norace +func malloc(size uintptr) unsafe.Pointer { + ret := call5(mallocABI0, uintptr(size), 0, 0, 0, 0) + // this indirection is to avoid go vet complaining about possible misuse of unsafe.Pointer + return *(*unsafe.Pointer)(unsafe.Pointer(&ret)) +} + +//go:nosplit +//go:norace +func free(ptr unsafe.Pointer) { + call5(freeABI0, uintptr(ptr), 0, 0, 0, 0) +} + +//go:nosplit +//go:norace +func setenv(name *byte, value *byte, overwrite int32) int32 { + return int32(call5(setenvABI0, uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(value)), uintptr(overwrite), 0, 0)) +} + +//go:nosplit +//go:norace +func unsetenv(name *byte) int32 { + return int32(call5(unsetenvABI0, uintptr(unsafe.Pointer(name)), 0, 0, 0, 0)) +} + +//go:nosplit +//go:norace +func sigfillset(set *sigset_t) int32 { + return int32(call5(sigfillsetABI0, uintptr(unsafe.Pointer(set)), 0, 0, 0, 0)) +} + +//go:nosplit +//go:norace +func nanosleep(ts *syscall.Timespec, rem *syscall.Timespec) int32 { + return int32(call5(nanosleepABI0, uintptr(unsafe.Pointer(ts)), uintptr(unsafe.Pointer(rem)), 0, 0, 0)) +} + +//go:nosplit +//go:norace +func abort() { + call5(abortABI0, 0, 0, 0, 0, 0) +} + +//go:nosplit +//go:norace +func sigaltstack(ss *stack_t, old_ss *stack_t) int32 { + return int32(call5(sigaltstackABI0, uintptr(unsafe.Pointer(ss)), uintptr(unsafe.Pointer(old_ss)), 0, 0, 0)) +} + +//go:nosplit +//go:norace +func pthread_attr_init(attr *pthread_attr_t) int32 { + return int32(call5(pthread_attr_initABI0, uintptr(unsafe.Pointer(attr)), 0, 0, 0, 0)) +} + +//go:nosplit +//go:norace +func pthread_create(thread *pthread_t, attr *pthread_attr_t, start unsafe.Pointer, arg unsafe.Pointer) int32 { + return int32(call5(pthread_createABI0, uintptr(unsafe.Pointer(thread)), uintptr(unsafe.Pointer(attr)), uintptr(start), uintptr(arg), 0)) +} + +//go:nosplit +//go:norace +func pthread_detach(thread pthread_t) int32 { + return int32(call5(pthread_detachABI0, uintptr(thread), 0, 0, 0, 0)) +} + +//go:nosplit +//go:norace +func pthread_sigmask(how sighow, ign *sigset_t, oset *sigset_t) int32 { + return int32(call5(pthread_sigmaskABI0, uintptr(how), uintptr(unsafe.Pointer(ign)), uintptr(unsafe.Pointer(oset)), 0, 0)) +} + +//go:nosplit +//go:norace +func pthread_self() pthread_t { + return pthread_t(call5(pthread_selfABI0, 0, 0, 0, 0, 0)) +} + +//go:nosplit +//go:norace +func pthread_get_stacksize_np(thread pthread_t) size_t { + return size_t(call5(pthread_get_stacksize_npABI0, uintptr(thread), 0, 0, 0, 0)) +} + +//go:nosplit +//go:norace +func pthread_attr_getstacksize(attr *pthread_attr_t, stacksize *size_t) int32 { + return int32(call5(pthread_attr_getstacksizeABI0, uintptr(unsafe.Pointer(attr)), uintptr(unsafe.Pointer(stacksize)), 0, 0, 0)) +} + +//go:nosplit +//go:norace +func pthread_attr_setstacksize(attr *pthread_attr_t, size size_t) int32 { + return int32(call5(pthread_attr_setstacksizeABI0, uintptr(unsafe.Pointer(attr)), uintptr(size), 0, 0, 0)) +} + +//go:nosplit +//go:norace +func pthread_attr_destroy(attr *pthread_attr_t) int32 { + return int32(call5(pthread_attr_destroyABI0, uintptr(unsafe.Pointer(attr)), 0, 0, 0, 0)) +} + +//go:nosplit +//go:norace +func pthread_mutex_lock(mutex *pthread_mutex_t) int32 { + return int32(call5(pthread_mutex_lockABI0, uintptr(unsafe.Pointer(mutex)), 0, 0, 0, 0)) +} + +//go:nosplit +//go:norace +func pthread_mutex_unlock(mutex *pthread_mutex_t) int32 { + return int32(call5(pthread_mutex_unlockABI0, uintptr(unsafe.Pointer(mutex)), 0, 0, 0, 0)) +} + +//go:nosplit +//go:norace +func pthread_cond_broadcast(cond *pthread_cond_t) int32 { + return int32(call5(pthread_cond_broadcastABI0, uintptr(unsafe.Pointer(cond)), 0, 0, 0, 0)) +} + +//go:nosplit +//go:norace +func pthread_setspecific(key pthread_key_t, value unsafe.Pointer) int32 { + return int32(call5(pthread_setspecificABI0, uintptr(key), uintptr(value), 0, 0, 0)) +} + +//go:linkname _malloc _malloc +var _malloc uint8 +var mallocABI0 = uintptr(unsafe.Pointer(&_malloc)) + +//go:linkname _free _free +var _free uint8 +var freeABI0 = uintptr(unsafe.Pointer(&_free)) + +//go:linkname _setenv _setenv +var _setenv uint8 +var setenvABI0 = uintptr(unsafe.Pointer(&_setenv)) + +//go:linkname _unsetenv _unsetenv +var _unsetenv uint8 +var unsetenvABI0 = uintptr(unsafe.Pointer(&_unsetenv)) + +//go:linkname _sigfillset _sigfillset +var _sigfillset uint8 +var sigfillsetABI0 = uintptr(unsafe.Pointer(&_sigfillset)) + +//go:linkname _nanosleep _nanosleep +var _nanosleep uint8 +var nanosleepABI0 = uintptr(unsafe.Pointer(&_nanosleep)) + +//go:linkname _abort _abort +var _abort uint8 +var abortABI0 = uintptr(unsafe.Pointer(&_abort)) + +//go:linkname _sigaltstack _sigaltstack +var _sigaltstack uint8 +var sigaltstackABI0 = uintptr(unsafe.Pointer(&_sigaltstack)) + +//go:linkname _pthread_attr_init _pthread_attr_init +var _pthread_attr_init uint8 +var pthread_attr_initABI0 = uintptr(unsafe.Pointer(&_pthread_attr_init)) + +//go:linkname _pthread_create _pthread_create +var _pthread_create uint8 +var pthread_createABI0 = uintptr(unsafe.Pointer(&_pthread_create)) + +//go:linkname _pthread_detach _pthread_detach +var _pthread_detach uint8 +var pthread_detachABI0 = uintptr(unsafe.Pointer(&_pthread_detach)) + +//go:linkname _pthread_sigmask _pthread_sigmask +var _pthread_sigmask uint8 +var pthread_sigmaskABI0 = uintptr(unsafe.Pointer(&_pthread_sigmask)) + +//go:linkname _pthread_self _pthread_self +var _pthread_self uint8 +var pthread_selfABI0 = uintptr(unsafe.Pointer(&_pthread_self)) + +//go:linkname _pthread_get_stacksize_np _pthread_get_stacksize_np +var _pthread_get_stacksize_np uint8 +var pthread_get_stacksize_npABI0 = uintptr(unsafe.Pointer(&_pthread_get_stacksize_np)) + +//go:linkname _pthread_attr_getstacksize _pthread_attr_getstacksize +var _pthread_attr_getstacksize uint8 +var pthread_attr_getstacksizeABI0 = uintptr(unsafe.Pointer(&_pthread_attr_getstacksize)) + +//go:linkname _pthread_attr_setstacksize _pthread_attr_setstacksize +var _pthread_attr_setstacksize uint8 +var pthread_attr_setstacksizeABI0 = uintptr(unsafe.Pointer(&_pthread_attr_setstacksize)) + +//go:linkname _pthread_attr_destroy _pthread_attr_destroy +var _pthread_attr_destroy uint8 +var pthread_attr_destroyABI0 = uintptr(unsafe.Pointer(&_pthread_attr_destroy)) + +//go:linkname _pthread_mutex_lock _pthread_mutex_lock +var _pthread_mutex_lock uint8 +var pthread_mutex_lockABI0 = uintptr(unsafe.Pointer(&_pthread_mutex_lock)) + +//go:linkname _pthread_mutex_unlock _pthread_mutex_unlock +var _pthread_mutex_unlock uint8 +var pthread_mutex_unlockABI0 = uintptr(unsafe.Pointer(&_pthread_mutex_unlock)) + +//go:linkname _pthread_cond_broadcast _pthread_cond_broadcast +var _pthread_cond_broadcast uint8 +var pthread_cond_broadcastABI0 = uintptr(unsafe.Pointer(&_pthread_cond_broadcast)) + +//go:linkname _pthread_setspecific _pthread_setspecific +var _pthread_setspecific uint8 +var pthread_setspecificABI0 = uintptr(unsafe.Pointer(&_pthread_setspecific)) diff --git a/vendor/github.com/ebitengine/purego/internal/fakecgo/symbols_darwin.go b/vendor/github.com/ebitengine/purego/internal/fakecgo/symbols_darwin.go new file mode 100644 index 0000000..8c4489f --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/fakecgo/symbols_darwin.go @@ -0,0 +1,30 @@ +// Code generated by 'go generate' with gen.go. DO NOT EDIT. + +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2022 The Ebitengine Authors + +//go:build !cgo + +package fakecgo + +//go:cgo_import_dynamic purego_malloc malloc "/usr/lib/libSystem.B.dylib" +//go:cgo_import_dynamic purego_free free "/usr/lib/libSystem.B.dylib" +//go:cgo_import_dynamic purego_setenv setenv "/usr/lib/libSystem.B.dylib" +//go:cgo_import_dynamic purego_unsetenv unsetenv "/usr/lib/libSystem.B.dylib" +//go:cgo_import_dynamic purego_sigfillset sigfillset "/usr/lib/libSystem.B.dylib" +//go:cgo_import_dynamic purego_nanosleep nanosleep "/usr/lib/libSystem.B.dylib" +//go:cgo_import_dynamic purego_abort abort "/usr/lib/libSystem.B.dylib" +//go:cgo_import_dynamic purego_sigaltstack sigaltstack "/usr/lib/libSystem.B.dylib" +//go:cgo_import_dynamic purego_pthread_attr_init pthread_attr_init "/usr/lib/libSystem.B.dylib" +//go:cgo_import_dynamic purego_pthread_create pthread_create "/usr/lib/libSystem.B.dylib" +//go:cgo_import_dynamic purego_pthread_detach pthread_detach "/usr/lib/libSystem.B.dylib" +//go:cgo_import_dynamic purego_pthread_sigmask pthread_sigmask "/usr/lib/libSystem.B.dylib" +//go:cgo_import_dynamic purego_pthread_self pthread_self "/usr/lib/libSystem.B.dylib" +//go:cgo_import_dynamic purego_pthread_get_stacksize_np pthread_get_stacksize_np "/usr/lib/libSystem.B.dylib" +//go:cgo_import_dynamic purego_pthread_attr_getstacksize pthread_attr_getstacksize "/usr/lib/libSystem.B.dylib" +//go:cgo_import_dynamic purego_pthread_attr_setstacksize pthread_attr_setstacksize "/usr/lib/libSystem.B.dylib" +//go:cgo_import_dynamic purego_pthread_attr_destroy pthread_attr_destroy "/usr/lib/libSystem.B.dylib" +//go:cgo_import_dynamic purego_pthread_mutex_lock pthread_mutex_lock "/usr/lib/libSystem.B.dylib" +//go:cgo_import_dynamic purego_pthread_mutex_unlock pthread_mutex_unlock "/usr/lib/libSystem.B.dylib" +//go:cgo_import_dynamic purego_pthread_cond_broadcast pthread_cond_broadcast "/usr/lib/libSystem.B.dylib" +//go:cgo_import_dynamic purego_pthread_setspecific pthread_setspecific "/usr/lib/libSystem.B.dylib" diff --git a/vendor/github.com/ebitengine/purego/internal/fakecgo/symbols_freebsd.go b/vendor/github.com/ebitengine/purego/internal/fakecgo/symbols_freebsd.go new file mode 100644 index 0000000..bbe1bd5 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/fakecgo/symbols_freebsd.go @@ -0,0 +1,30 @@ +// Code generated by 'go generate' with gen.go. DO NOT EDIT. + +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2022 The Ebitengine Authors + +//go:build !cgo + +package fakecgo + +//go:cgo_import_dynamic purego_malloc malloc "libc.so.7" +//go:cgo_import_dynamic purego_free free "libc.so.7" +//go:cgo_import_dynamic purego_setenv setenv "libc.so.7" +//go:cgo_import_dynamic purego_unsetenv unsetenv "libc.so.7" +//go:cgo_import_dynamic purego_sigfillset sigfillset "libc.so.7" +//go:cgo_import_dynamic purego_nanosleep nanosleep "libc.so.7" +//go:cgo_import_dynamic purego_abort abort "libc.so.7" +//go:cgo_import_dynamic purego_sigaltstack sigaltstack "libc.so.7" +//go:cgo_import_dynamic purego_pthread_attr_init pthread_attr_init "libpthread.so" +//go:cgo_import_dynamic purego_pthread_create pthread_create "libpthread.so" +//go:cgo_import_dynamic purego_pthread_detach pthread_detach "libpthread.so" +//go:cgo_import_dynamic purego_pthread_sigmask pthread_sigmask "libpthread.so" +//go:cgo_import_dynamic purego_pthread_self pthread_self "libpthread.so" +//go:cgo_import_dynamic purego_pthread_get_stacksize_np pthread_get_stacksize_np "libpthread.so" +//go:cgo_import_dynamic purego_pthread_attr_getstacksize pthread_attr_getstacksize "libpthread.so" +//go:cgo_import_dynamic purego_pthread_attr_setstacksize pthread_attr_setstacksize "libpthread.so" +//go:cgo_import_dynamic purego_pthread_attr_destroy pthread_attr_destroy "libpthread.so" +//go:cgo_import_dynamic purego_pthread_mutex_lock pthread_mutex_lock "libpthread.so" +//go:cgo_import_dynamic purego_pthread_mutex_unlock pthread_mutex_unlock "libpthread.so" +//go:cgo_import_dynamic purego_pthread_cond_broadcast pthread_cond_broadcast "libpthread.so" +//go:cgo_import_dynamic purego_pthread_setspecific pthread_setspecific "libpthread.so" diff --git a/vendor/github.com/ebitengine/purego/internal/fakecgo/symbols_linux.go b/vendor/github.com/ebitengine/purego/internal/fakecgo/symbols_linux.go new file mode 100644 index 0000000..2165265 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/fakecgo/symbols_linux.go @@ -0,0 +1,30 @@ +// Code generated by 'go generate' with gen.go. DO NOT EDIT. + +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2022 The Ebitengine Authors + +//go:build !cgo + +package fakecgo + +//go:cgo_import_dynamic purego_malloc malloc "libc.so.6" +//go:cgo_import_dynamic purego_free free "libc.so.6" +//go:cgo_import_dynamic purego_setenv setenv "libc.so.6" +//go:cgo_import_dynamic purego_unsetenv unsetenv "libc.so.6" +//go:cgo_import_dynamic purego_sigfillset sigfillset "libc.so.6" +//go:cgo_import_dynamic purego_nanosleep nanosleep "libc.so.6" +//go:cgo_import_dynamic purego_abort abort "libc.so.6" +//go:cgo_import_dynamic purego_sigaltstack sigaltstack "libc.so.6" +//go:cgo_import_dynamic purego_pthread_attr_init pthread_attr_init "libpthread.so.0" +//go:cgo_import_dynamic purego_pthread_create pthread_create "libpthread.so.0" +//go:cgo_import_dynamic purego_pthread_detach pthread_detach "libpthread.so.0" +//go:cgo_import_dynamic purego_pthread_sigmask pthread_sigmask "libpthread.so.0" +//go:cgo_import_dynamic purego_pthread_self pthread_self "libpthread.so.0" +//go:cgo_import_dynamic purego_pthread_get_stacksize_np pthread_get_stacksize_np "libpthread.so.0" +//go:cgo_import_dynamic purego_pthread_attr_getstacksize pthread_attr_getstacksize "libpthread.so.0" +//go:cgo_import_dynamic purego_pthread_attr_setstacksize pthread_attr_setstacksize "libpthread.so.0" +//go:cgo_import_dynamic purego_pthread_attr_destroy pthread_attr_destroy "libpthread.so.0" +//go:cgo_import_dynamic purego_pthread_mutex_lock pthread_mutex_lock "libpthread.so.0" +//go:cgo_import_dynamic purego_pthread_mutex_unlock pthread_mutex_unlock "libpthread.so.0" +//go:cgo_import_dynamic purego_pthread_cond_broadcast pthread_cond_broadcast "libpthread.so.0" +//go:cgo_import_dynamic purego_pthread_setspecific pthread_setspecific "libpthread.so.0" diff --git a/vendor/github.com/ebitengine/purego/internal/fakecgo/symbols_netbsd.go b/vendor/github.com/ebitengine/purego/internal/fakecgo/symbols_netbsd.go new file mode 100644 index 0000000..7c92bb0 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/fakecgo/symbols_netbsd.go @@ -0,0 +1,30 @@ +// Code generated by 'go generate' with gen.go. DO NOT EDIT. + +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2022 The Ebitengine Authors + +//go:build !cgo + +package fakecgo + +//go:cgo_import_dynamic purego_malloc malloc "libc.so" +//go:cgo_import_dynamic purego_free free "libc.so" +//go:cgo_import_dynamic purego_setenv setenv "libc.so" +//go:cgo_import_dynamic purego_unsetenv unsetenv "libc.so" +//go:cgo_import_dynamic purego_sigfillset sigfillset "libc.so" +//go:cgo_import_dynamic purego_nanosleep nanosleep "libc.so" +//go:cgo_import_dynamic purego_abort abort "libc.so" +//go:cgo_import_dynamic purego_sigaltstack sigaltstack "libc.so" +//go:cgo_import_dynamic purego_pthread_attr_init pthread_attr_init "libpthread.so" +//go:cgo_import_dynamic purego_pthread_create pthread_create "libpthread.so" +//go:cgo_import_dynamic purego_pthread_detach pthread_detach "libpthread.so" +//go:cgo_import_dynamic purego_pthread_sigmask pthread_sigmask "libpthread.so" +//go:cgo_import_dynamic purego_pthread_self pthread_self "libpthread.so" +//go:cgo_import_dynamic purego_pthread_get_stacksize_np pthread_get_stacksize_np "libpthread.so" +//go:cgo_import_dynamic purego_pthread_attr_getstacksize pthread_attr_getstacksize "libpthread.so" +//go:cgo_import_dynamic purego_pthread_attr_setstacksize pthread_attr_setstacksize "libpthread.so" +//go:cgo_import_dynamic purego_pthread_attr_destroy pthread_attr_destroy "libpthread.so" +//go:cgo_import_dynamic purego_pthread_mutex_lock pthread_mutex_lock "libpthread.so" +//go:cgo_import_dynamic purego_pthread_mutex_unlock pthread_mutex_unlock "libpthread.so" +//go:cgo_import_dynamic purego_pthread_cond_broadcast pthread_cond_broadcast "libpthread.so" +//go:cgo_import_dynamic purego_pthread_setspecific pthread_setspecific "libpthread.so" diff --git a/vendor/github.com/ebitengine/purego/internal/fakecgo/trampolines_amd64.s b/vendor/github.com/ebitengine/purego/internal/fakecgo/trampolines_amd64.s new file mode 100644 index 0000000..c9a3cc0 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/fakecgo/trampolines_amd64.s @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2022 The Ebitengine Authors + +//go:build !cgo && (darwin || linux || freebsd) + +/* +trampoline for emulating required C functions for cgo in go (see cgo.go) +(we convert cdecl calling convention to go and vice-versa) + +Since we're called from go and call into C we can cheat a bit with the calling conventions: + - in go all the registers are caller saved + - in C we have a couple of callee saved registers + +=> we can use BX, R12, R13, R14, R15 instead of the stack + +C Calling convention cdecl used here (we only need integer args): +1. arg: DI +2. arg: SI +3. arg: DX +4. arg: CX +5. arg: R8 +6. arg: R9 +We don't need floats with these functions -> AX=0 +return value will be in AX +*/ +#include "textflag.h" +#include "go_asm.h" + +// these trampolines map the gcc ABI to Go ABI and then calls into the Go equivalent functions. + +TEXT x_cgo_init_trampoline(SB), NOSPLIT, $16 + MOVQ DI, AX + MOVQ SI, BX + MOVQ ·x_cgo_init_call(SB), DX + MOVQ (DX), CX + CALL CX + RET + +TEXT x_cgo_thread_start_trampoline(SB), NOSPLIT, $8 + MOVQ DI, AX + MOVQ ·x_cgo_thread_start_call(SB), DX + MOVQ (DX), CX + CALL CX + RET + +TEXT x_cgo_setenv_trampoline(SB), NOSPLIT, $8 + MOVQ DI, AX + MOVQ ·x_cgo_setenv_call(SB), DX + MOVQ (DX), CX + CALL CX + RET + +TEXT x_cgo_unsetenv_trampoline(SB), NOSPLIT, $8 + MOVQ DI, AX + MOVQ ·x_cgo_unsetenv_call(SB), DX + MOVQ (DX), CX + CALL CX + RET + +TEXT x_cgo_notify_runtime_init_done_trampoline(SB), NOSPLIT, $0 + CALL ·x_cgo_notify_runtime_init_done(SB) + RET + +TEXT x_cgo_bindm_trampoline(SB), NOSPLIT, $0 + CALL ·x_cgo_bindm(SB) + RET + +// func setg_trampoline(setg uintptr, g uintptr) +TEXT ·setg_trampoline(SB), NOSPLIT, $0-16 + MOVQ G+8(FP), DI + MOVQ setg+0(FP), BX + XORL AX, AX + CALL BX + RET + +TEXT threadentry_trampoline(SB), NOSPLIT, $16 + MOVQ DI, AX + MOVQ ·threadentry_call(SB), DX + MOVQ (DX), CX + CALL CX + RET + +TEXT ·call5(SB), NOSPLIT, $0-56 + MOVQ fn+0(FP), BX + MOVQ a1+8(FP), DI + MOVQ a2+16(FP), SI + MOVQ a3+24(FP), DX + MOVQ a4+32(FP), CX + MOVQ a5+40(FP), R8 + + XORL AX, AX // no floats + + PUSHQ BP // save BP + MOVQ SP, BP // save SP inside BP bc BP is callee-saved + SUBQ $16, SP // allocate space for alignment + ANDQ $-16, SP // align on 16 bytes for SSE + + CALL BX + + MOVQ BP, SP // get SP back + POPQ BP // restore BP + + MOVQ AX, ret+48(FP) + RET diff --git a/vendor/github.com/ebitengine/purego/internal/fakecgo/trampolines_arm64.s b/vendor/github.com/ebitengine/purego/internal/fakecgo/trampolines_arm64.s new file mode 100644 index 0000000..9dbdbc0 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/fakecgo/trampolines_arm64.s @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2022 The Ebitengine Authors + +//go:build !cgo && (darwin || freebsd || linux) + +#include "textflag.h" +#include "go_asm.h" + +// these trampolines map the gcc ABI to Go ABI and then calls into the Go equivalent functions. + +TEXT x_cgo_init_trampoline(SB), NOSPLIT, $0-0 + MOVD R0, 8(RSP) + MOVD R1, 16(RSP) + MOVD ·x_cgo_init_call(SB), R26 + MOVD (R26), R2 + CALL (R2) + RET + +TEXT x_cgo_thread_start_trampoline(SB), NOSPLIT, $0-0 + MOVD R0, 8(RSP) + MOVD ·x_cgo_thread_start_call(SB), R26 + MOVD (R26), R2 + CALL (R2) + RET + +TEXT x_cgo_setenv_trampoline(SB), NOSPLIT, $0-0 + MOVD R0, 8(RSP) + MOVD ·x_cgo_setenv_call(SB), R26 + MOVD (R26), R2 + CALL (R2) + RET + +TEXT x_cgo_unsetenv_trampoline(SB), NOSPLIT, $0-0 + MOVD R0, 8(RSP) + MOVD ·x_cgo_unsetenv_call(SB), R26 + MOVD (R26), R2 + CALL (R2) + RET + +TEXT x_cgo_notify_runtime_init_done_trampoline(SB), NOSPLIT, $0-0 + CALL ·x_cgo_notify_runtime_init_done(SB) + RET + +TEXT x_cgo_bindm_trampoline(SB), NOSPLIT, $0 + CALL ·x_cgo_bindm(SB) + RET + +// func setg_trampoline(setg uintptr, g uintptr) +TEXT ·setg_trampoline(SB), NOSPLIT, $0-16 + MOVD G+8(FP), R0 + MOVD setg+0(FP), R1 + CALL R1 + RET + +TEXT threadentry_trampoline(SB), NOSPLIT, $0-0 + MOVD R0, 8(RSP) + MOVD ·threadentry_call(SB), R26 + MOVD (R26), R2 + CALL (R2) + MOVD $0, R0 // TODO: get the return value from threadentry + RET + +TEXT ·call5(SB), NOSPLIT, $0-0 + MOVD fn+0(FP), R6 + MOVD a1+8(FP), R0 + MOVD a2+16(FP), R1 + MOVD a3+24(FP), R2 + MOVD a4+32(FP), R3 + MOVD a5+40(FP), R4 + CALL R6 + MOVD R0, ret+48(FP) + RET diff --git a/vendor/github.com/ebitengine/purego/internal/fakecgo/trampolines_loong64.s b/vendor/github.com/ebitengine/purego/internal/fakecgo/trampolines_loong64.s new file mode 100644 index 0000000..15b3354 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/fakecgo/trampolines_loong64.s @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 The Ebitengine Authors + +//go:build !cgo && linux + +#include "textflag.h" +#include "go_asm.h" + +// these trampolines map the gcc ABI to Go ABI and then calls into the Go equivalent functions. + +TEXT x_cgo_init_trampoline(SB), NOSPLIT, $16 + MOVV R4, 8(R3) + MOVV R5, 16(R3) + MOVV ·x_cgo_init_call(SB), R6 + MOVV (R6), R7 + CALL (R7) + RET + +TEXT x_cgo_thread_start_trampoline(SB), NOSPLIT, $8 + MOVV R4, 8(R3) + MOVV ·x_cgo_thread_start_call(SB), R5 + MOVV (R5), R6 + CALL (R6) + RET + +TEXT x_cgo_setenv_trampoline(SB), NOSPLIT, $8 + MOVV R4, 8(R3) + MOVV ·x_cgo_setenv_call(SB), R5 + MOVV (R5), R6 + CALL (R6) + RET + +TEXT x_cgo_unsetenv_trampoline(SB), NOSPLIT, $8 + MOVV R4, 8(R3) + MOVV ·x_cgo_unsetenv_call(SB), R5 + MOVV (R5), R6 + CALL (R6) + RET + +TEXT x_cgo_notify_runtime_init_done_trampoline(SB), NOSPLIT, $0 + CALL ·x_cgo_notify_runtime_init_done(SB) + RET + +TEXT x_cgo_bindm_trampoline(SB), NOSPLIT, $0 + CALL ·x_cgo_bindm(SB) + RET + +// func setg_trampoline(setg uintptr, g uintptr) +TEXT ·setg_trampoline(SB), NOSPLIT, $0 + MOVV G+8(FP), R4 + MOVV setg+0(FP), R5 + CALL (R5) + RET + +TEXT threadentry_trampoline(SB), NOSPLIT, $16 + MOVV R4, 8(R3) + MOVV ·threadentry_call(SB), R5 + MOVV (R5), R6 + CALL (R6) + RET + +TEXT ·call5(SB), NOSPLIT, $0-0 + MOVV fn+0(FP), R9 + MOVV a1+8(FP), R4 + MOVV a2+16(FP), R5 + MOVV a3+24(FP), R6 + MOVV a4+32(FP), R7 + MOVV a5+40(FP), R8 + CALL (R9) + MOVV R4, ret+48(FP) + RET diff --git a/vendor/github.com/ebitengine/purego/internal/fakecgo/trampolines_stubs.s b/vendor/github.com/ebitengine/purego/internal/fakecgo/trampolines_stubs.s new file mode 100644 index 0000000..c93d783 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/fakecgo/trampolines_stubs.s @@ -0,0 +1,94 @@ +// Code generated by 'go generate' with gen.go. DO NOT EDIT. + +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2022 The Ebitengine Authors + +//go:build !cgo && (darwin || freebsd || linux || netbsd) + +#include "textflag.h" + +// these stubs are here because it is not possible to go:linkname directly the C functions on darwin arm64 + +TEXT _malloc(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_malloc(SB) + RET + +TEXT _free(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_free(SB) + RET + +TEXT _setenv(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_setenv(SB) + RET + +TEXT _unsetenv(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_unsetenv(SB) + RET + +TEXT _sigfillset(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_sigfillset(SB) + RET + +TEXT _nanosleep(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_nanosleep(SB) + RET + +TEXT _abort(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_abort(SB) + RET + +TEXT _sigaltstack(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_sigaltstack(SB) + RET + +TEXT _pthread_attr_init(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_pthread_attr_init(SB) + RET + +TEXT _pthread_create(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_pthread_create(SB) + RET + +TEXT _pthread_detach(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_pthread_detach(SB) + RET + +TEXT _pthread_sigmask(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_pthread_sigmask(SB) + RET + +TEXT _pthread_self(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_pthread_self(SB) + RET + +TEXT _pthread_get_stacksize_np(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_pthread_get_stacksize_np(SB) + RET + +TEXT _pthread_attr_getstacksize(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_pthread_attr_getstacksize(SB) + RET + +TEXT _pthread_attr_setstacksize(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_pthread_attr_setstacksize(SB) + RET + +TEXT _pthread_attr_destroy(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_pthread_attr_destroy(SB) + RET + +TEXT _pthread_mutex_lock(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_pthread_mutex_lock(SB) + RET + +TEXT _pthread_mutex_unlock(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_pthread_mutex_unlock(SB) + RET + +TEXT _pthread_cond_broadcast(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_pthread_cond_broadcast(SB) + RET + +TEXT _pthread_setspecific(SB), NOSPLIT|NOFRAME, $0-0 + JMP purego_pthread_setspecific(SB) + RET diff --git a/vendor/github.com/ebitengine/purego/internal/strings/strings.go b/vendor/github.com/ebitengine/purego/internal/strings/strings.go new file mode 100644 index 0000000..5b0d252 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/internal/strings/strings.go @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2022 The Ebitengine Authors + +package strings + +import ( + "unsafe" +) + +// hasSuffix tests whether the string s ends with suffix. +func hasSuffix(s, suffix string) bool { + return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix +} + +// CString converts a go string to *byte that can be passed to C code. +func CString(name string) *byte { + if hasSuffix(name, "\x00") { + return &(*(*[]byte)(unsafe.Pointer(&name)))[0] + } + b := make([]byte, len(name)+1) + copy(b, name) + return &b[0] +} + +// GoString copies a null-terminated char* to a Go string. +func GoString(c uintptr) string { + // We take the address and then dereference it to trick go vet from creating a possible misuse of unsafe.Pointer + ptr := *(*unsafe.Pointer)(unsafe.Pointer(&c)) + if ptr == nil { + return "" + } + var length int + for { + if *(*byte)(unsafe.Add(ptr, uintptr(length))) == '\x00' { + break + } + length++ + } + return string(unsafe.Slice((*byte)(ptr), length)) +} diff --git a/vendor/github.com/ebitengine/purego/is_ios.go b/vendor/github.com/ebitengine/purego/is_ios.go new file mode 100644 index 0000000..ed31da9 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/is_ios.go @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2022 The Ebitengine Authors + +//go:build !cgo + +package purego + +// if you are getting this error it means that you have +// CGO_ENABLED=0 while trying to build for ios. +// purego does not support this mode yet. +// the fix is to set CGO_ENABLED=1 which will require +// a C compiler. +var _ = _PUREGO_REQUIRES_CGO_ON_IOS diff --git a/vendor/github.com/ebitengine/purego/nocgo.go b/vendor/github.com/ebitengine/purego/nocgo.go new file mode 100644 index 0000000..b91b979 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/nocgo.go @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2022 The Ebitengine Authors + +//go:build !cgo && (darwin || freebsd || linux || netbsd) + +package purego + +// if CGO_ENABLED=0 import fakecgo to setup the Cgo runtime correctly. +// This is required since some frameworks need TLS setup the C way which Go doesn't do. +// We currently don't support ios in fakecgo mode so force Cgo or fail +// +// The way that the Cgo runtime (runtime/cgo) works is by setting some variables found +// in runtime with non-null GCC compiled functions. The variables that are replaced are +// var ( +// iscgo bool // in runtime/cgo.go +// _cgo_init unsafe.Pointer // in runtime/cgo.go +// _cgo_thread_start unsafe.Pointer // in runtime/cgo.go +// _cgo_notify_runtime_init_done unsafe.Pointer // in runtime/cgo.go +// _cgo_setenv unsafe.Pointer // in runtime/env_posix.go +// _cgo_unsetenv unsafe.Pointer // in runtime/env_posix.go +// ) +// importing fakecgo will set these (using //go:linkname) with functions written +// entirely in Go (except for some assembly trampolines to change GCC ABI to Go ABI). +// Doing so makes it possible to build applications that call into C without CGO_ENABLED=1. +import _ "github.com/ebitengine/purego/internal/fakecgo" diff --git a/vendor/github.com/ebitengine/purego/struct_amd64.go b/vendor/github.com/ebitengine/purego/struct_amd64.go new file mode 100644 index 0000000..bd6c977 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/struct_amd64.go @@ -0,0 +1,264 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2024 The Ebitengine Authors + +package purego + +import ( + "math" + "reflect" + "unsafe" +) + +func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) { + outSize := outType.Size() + switch { + case outSize == 0: + return reflect.New(outType).Elem() + case outSize <= 8: + if isAllFloats(outType) { + // 2 float32s or 1 float64s are return in the float register + return reflect.NewAt(outType, unsafe.Pointer(&struct{ a uintptr }{syscall.f1})).Elem() + } + // up to 8 bytes is returned in RAX + return reflect.NewAt(outType, unsafe.Pointer(&struct{ a uintptr }{syscall.a1})).Elem() + case outSize <= 16: + r1, r2 := syscall.a1, syscall.a2 + if isAllFloats(outType) { + r1 = syscall.f1 + r2 = syscall.f2 + } else { + // check first 8 bytes if it's floats + hasFirstFloat := false + f1 := outType.Field(0).Type + if f1.Kind() == reflect.Float64 || f1.Kind() == reflect.Float32 && outType.Field(1).Type.Kind() == reflect.Float32 { + r1 = syscall.f1 + hasFirstFloat = true + } + + // find index of the field that starts the second 8 bytes + var i int + for i = 0; i < outType.NumField(); i++ { + if outType.Field(i).Offset == 8 { + break + } + } + + // check last 8 bytes if they are floats + f1 = outType.Field(i).Type + if f1.Kind() == reflect.Float64 || f1.Kind() == reflect.Float32 && i+1 == outType.NumField() { + r2 = syscall.f1 + } else if hasFirstFloat { + // if the first field was a float then that means the second integer field + // comes from the first integer register + r2 = syscall.a1 + } + } + return reflect.NewAt(outType, unsafe.Pointer(&struct{ a, b uintptr }{r1, r2})).Elem() + default: + // create struct from the Go pointer created above + // weird pointer dereference to circumvent go vet + return reflect.NewAt(outType, *(*unsafe.Pointer)(unsafe.Pointer(&syscall.a1))).Elem() + } +} + +func isAllFloats(ty reflect.Type) bool { + for i := 0; i < ty.NumField(); i++ { + f := ty.Field(i) + switch f.Type.Kind() { + case reflect.Float64, reflect.Float32: + default: + return false + } + } + return true +} + +// https://refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf +// https://gitlab.com/x86-psABIs/x86-64-ABI +// Class determines where the 8 byte value goes. +// Higher value classes win over lower value classes +const ( + _NO_CLASS = 0b0000 + _SSE = 0b0001 + _X87 = 0b0011 // long double not used in Go + _INTEGER = 0b0111 + _MEMORY = 0b1111 +) + +func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFloat, addStack func(uintptr), keepAlive []any) []any { + if v.Type().Size() == 0 { + return keepAlive + } + + // if greater than 64 bytes place on stack + if v.Type().Size() > 8*8 { + placeStack(v, addStack) + return keepAlive + } + var ( + savedNumFloats = *numFloats + savedNumInts = *numInts + savedNumStack = *numStack + ) + placeOnStack := postMerger(v.Type()) || !tryPlaceRegister(v, addFloat, addInt) + if placeOnStack { + // reset any values placed in registers + *numFloats = savedNumFloats + *numInts = savedNumInts + *numStack = savedNumStack + placeStack(v, addStack) + } + return keepAlive +} + +func postMerger(t reflect.Type) (passInMemory bool) { + // (c) If the size of the aggregate exceeds two eightbytes and the first eight- byte isn’t SSE or any other + // eightbyte isn’t SSEUP, the whole argument is passed in memory. + if t.Kind() != reflect.Struct { + return false + } + if t.Size() <= 2*8 { + return false + } + return true // Go does not have an SSE/SSEUP type so this is always true +} + +func tryPlaceRegister(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) (ok bool) { + ok = true + var val uint64 + var shift byte // # of bits to shift + var flushed bool + class := _NO_CLASS + flushIfNeeded := func() { + if flushed { + return + } + flushed = true + if class == _SSE { + addFloat(uintptr(val)) + } else { + addInt(uintptr(val)) + } + val = 0 + shift = 0 + class = _NO_CLASS + } + var place func(v reflect.Value) + place = func(v reflect.Value) { + var numFields int + if v.Kind() == reflect.Struct { + numFields = v.Type().NumField() + } else { + numFields = v.Type().Len() + } + + for i := 0; i < numFields; i++ { + flushed = false + var f reflect.Value + if v.Kind() == reflect.Struct { + f = v.Field(i) + } else { + f = v.Index(i) + } + switch f.Kind() { + case reflect.Struct: + place(f) + case reflect.Bool: + if f.Bool() { + val |= 1 + } + shift += 8 + class |= _INTEGER + case reflect.Pointer: + ok = false + return + case reflect.Int8: + val |= uint64(f.Int()&0xFF) << shift + shift += 8 + class |= _INTEGER + case reflect.Int16: + val |= uint64(f.Int()&0xFFFF) << shift + shift += 16 + class |= _INTEGER + case reflect.Int32: + val |= uint64(f.Int()&0xFFFF_FFFF) << shift + shift += 32 + class |= _INTEGER + case reflect.Int64, reflect.Int: + val = uint64(f.Int()) + shift = 64 + class = _INTEGER + case reflect.Uint8: + val |= f.Uint() << shift + shift += 8 + class |= _INTEGER + case reflect.Uint16: + val |= f.Uint() << shift + shift += 16 + class |= _INTEGER + case reflect.Uint32: + val |= f.Uint() << shift + shift += 32 + class |= _INTEGER + case reflect.Uint64, reflect.Uint, reflect.Uintptr: + val = f.Uint() + shift = 64 + class = _INTEGER + case reflect.Float32: + val |= uint64(math.Float32bits(float32(f.Float()))) << shift + shift += 32 + class |= _SSE + case reflect.Float64: + if v.Type().Size() > 16 { + ok = false + return + } + val = uint64(math.Float64bits(f.Float())) + shift = 64 + class = _SSE + case reflect.Array: + place(f) + default: + panic("purego: unsupported kind " + f.Kind().String()) + } + + if shift == 64 { + flushIfNeeded() + } else if shift > 64 { + // Should never happen, but may if we forget to reset shift after flush (or forget to flush), + // better fall apart here, than corrupt arguments. + panic("purego: tryPlaceRegisters shift > 64") + } + } + } + + place(v) + flushIfNeeded() + return ok +} + +func placeStack(v reflect.Value, addStack func(uintptr)) { + for i := 0; i < v.Type().NumField(); i++ { + f := v.Field(i) + switch f.Kind() { + case reflect.Pointer: + addStack(f.Pointer()) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + addStack(uintptr(f.Int())) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + addStack(uintptr(f.Uint())) + case reflect.Float32: + addStack(uintptr(math.Float32bits(float32(f.Float())))) + case reflect.Float64: + addStack(uintptr(math.Float64bits(f.Float()))) + case reflect.Struct: + placeStack(f, addStack) + default: + panic("purego: unsupported kind " + f.Kind().String()) + } + } +} + +func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) { + panic("purego: not needed on amd64") +} diff --git a/vendor/github.com/ebitengine/purego/struct_arm64.go b/vendor/github.com/ebitengine/purego/struct_arm64.go new file mode 100644 index 0000000..6c73e98 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/struct_arm64.go @@ -0,0 +1,284 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2024 The Ebitengine Authors + +package purego + +import ( + "math" + "reflect" + "unsafe" +) + +func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) { + outSize := outType.Size() + switch { + case outSize == 0: + return reflect.New(outType).Elem() + case outSize <= 8: + r1 := syscall.a1 + if isAllFloats, numFields := isAllSameFloat(outType); isAllFloats { + r1 = syscall.f1 + if numFields == 2 { + r1 = syscall.f2<<32 | syscall.f1 + } + } + return reflect.NewAt(outType, unsafe.Pointer(&struct{ a uintptr }{r1})).Elem() + case outSize <= 16: + r1, r2 := syscall.a1, syscall.a2 + if isAllFloats, numFields := isAllSameFloat(outType); isAllFloats { + switch numFields { + case 4: + r1 = syscall.f2<<32 | syscall.f1 + r2 = syscall.f4<<32 | syscall.f3 + case 3: + r1 = syscall.f2<<32 | syscall.f1 + r2 = syscall.f3 + case 2: + r1 = syscall.f1 + r2 = syscall.f2 + default: + panic("unreachable") + } + } + return reflect.NewAt(outType, unsafe.Pointer(&struct{ a, b uintptr }{r1, r2})).Elem() + default: + if isAllFloats, numFields := isAllSameFloat(outType); isAllFloats && numFields <= 4 { + switch numFields { + case 4: + return reflect.NewAt(outType, unsafe.Pointer(&struct{ a, b, c, d uintptr }{syscall.f1, syscall.f2, syscall.f3, syscall.f4})).Elem() + case 3: + return reflect.NewAt(outType, unsafe.Pointer(&struct{ a, b, c uintptr }{syscall.f1, syscall.f2, syscall.f3})).Elem() + default: + panic("unreachable") + } + } + // create struct from the Go pointer created in arm64_r8 + // weird pointer dereference to circumvent go vet + return reflect.NewAt(outType, *(*unsafe.Pointer)(unsafe.Pointer(&syscall.arm64_r8))).Elem() + } +} + +// https://github.com/ARM-software/abi-aa/blob/main/sysvabi64/sysvabi64.rst +const ( + _NO_CLASS = 0b00 + _FLOAT = 0b01 + _INT = 0b11 +) + +func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFloat, addStack func(uintptr), keepAlive []any) []any { + if v.Type().Size() == 0 { + return keepAlive + } + + if hva, hfa, size := isHVA(v.Type()), isHFA(v.Type()), v.Type().Size(); hva || hfa || size <= 16 { + // if this doesn't fit entirely in registers then + // each element goes onto the stack + if hfa && *numFloats+v.NumField() > numOfFloatRegisters { + *numFloats = numOfFloatRegisters + } else if hva && *numInts+v.NumField() > numOfIntegerRegisters() { + *numInts = numOfIntegerRegisters() + } + + placeRegisters(v, addFloat, addInt) + } else { + keepAlive = placeStack(v, keepAlive, addInt) + } + return keepAlive // the struct was allocated so don't panic +} + +func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) { + var val uint64 + var shift byte + var flushed bool + class := _NO_CLASS + var place func(v reflect.Value) + place = func(v reflect.Value) { + var numFields int + if v.Kind() == reflect.Struct { + numFields = v.Type().NumField() + } else { + numFields = v.Type().Len() + } + for k := 0; k < numFields; k++ { + flushed = false + var f reflect.Value + if v.Kind() == reflect.Struct { + f = v.Field(k) + } else { + f = v.Index(k) + } + align := byte(f.Type().Align()*8 - 1) + shift = (shift + align) &^ align + if shift >= 64 { + shift = 0 + flushed = true + if class == _FLOAT { + addFloat(uintptr(val)) + } else { + addInt(uintptr(val)) + } + } + switch f.Type().Kind() { + case reflect.Struct: + place(f) + case reflect.Bool: + if f.Bool() { + val |= 1 + } + shift += 8 + class |= _INT + case reflect.Uint8: + val |= f.Uint() << shift + shift += 8 + class |= _INT + case reflect.Uint16: + val |= f.Uint() << shift + shift += 16 + class |= _INT + case reflect.Uint32: + val |= f.Uint() << shift + shift += 32 + class |= _INT + case reflect.Uint64, reflect.Uint, reflect.Uintptr: + addInt(uintptr(f.Uint())) + shift = 0 + flushed = true + class = _NO_CLASS + case reflect.Int8: + val |= uint64(f.Int()&0xFF) << shift + shift += 8 + class |= _INT + case reflect.Int16: + val |= uint64(f.Int()&0xFFFF) << shift + shift += 16 + class |= _INT + case reflect.Int32: + val |= uint64(f.Int()&0xFFFF_FFFF) << shift + shift += 32 + class |= _INT + case reflect.Int64, reflect.Int: + addInt(uintptr(f.Int())) + shift = 0 + flushed = true + class = _NO_CLASS + case reflect.Float32: + if class == _FLOAT { + addFloat(uintptr(val)) + val = 0 + shift = 0 + } + val |= uint64(math.Float32bits(float32(f.Float()))) << shift + shift += 32 + class |= _FLOAT + case reflect.Float64: + addFloat(uintptr(math.Float64bits(float64(f.Float())))) + shift = 0 + flushed = true + class = _NO_CLASS + case reflect.Ptr: + addInt(f.Pointer()) + shift = 0 + flushed = true + class = _NO_CLASS + case reflect.Array: + place(f) + default: + panic("purego: unsupported kind " + f.Kind().String()) + } + } + } + place(v) + if !flushed { + if class == _FLOAT { + addFloat(uintptr(val)) + } else { + addInt(uintptr(val)) + } + } +} + +func placeStack(v reflect.Value, keepAlive []any, addInt func(uintptr)) []any { + // Struct is too big to be placed in registers. + // Copy to heap and place the pointer in register + ptrStruct := reflect.New(v.Type()) + ptrStruct.Elem().Set(v) + ptr := ptrStruct.Elem().Addr().UnsafePointer() + keepAlive = append(keepAlive, ptr) + addInt(uintptr(ptr)) + return keepAlive +} + +// isHFA reports a Homogeneous Floating-point Aggregate (HFA) which is a Fundamental Data Type that is a +// Floating-Point type and at most four uniquely addressable members (5.9.5.1 in [Arm64 Calling Convention]). +// This type of struct will be placed more compactly than the individual fields. +// +// [Arm64 Calling Convention]: https://github.com/ARM-software/abi-aa/blob/main/sysvabi64/sysvabi64.rst +func isHFA(t reflect.Type) bool { + // round up struct size to nearest 8 see section B.4 + structSize := roundUpTo8(t.Size()) + if structSize == 0 || t.NumField() > 4 { + return false + } + first := t.Field(0) + switch first.Type.Kind() { + case reflect.Float32, reflect.Float64: + firstKind := first.Type.Kind() + for i := 0; i < t.NumField(); i++ { + if t.Field(i).Type.Kind() != firstKind { + return false + } + } + return true + case reflect.Array: + switch first.Type.Elem().Kind() { + case reflect.Float32, reflect.Float64: + return true + default: + return false + } + case reflect.Struct: + for i := 0; i < first.Type.NumField(); i++ { + if !isHFA(first.Type) { + return false + } + } + return true + default: + return false + } +} + +// isHVA reports a Homogeneous Aggregate with a Fundamental Data Type that is a Short-Vector type +// and at most four uniquely addressable members (5.9.5.2 in [Arm64 Calling Convention]). +// A short vector is a machine type that is composed of repeated instances of one fundamental integral or +// floating-point type. It may be 8 or 16 bytes in total size (5.4 in [Arm64 Calling Convention]). +// This type of struct will be placed more compactly than the individual fields. +// +// [Arm64 Calling Convention]: https://github.com/ARM-software/abi-aa/blob/main/sysvabi64/sysvabi64.rst +func isHVA(t reflect.Type) bool { + // round up struct size to nearest 8 see section B.4 + structSize := roundUpTo8(t.Size()) + if structSize == 0 || (structSize != 8 && structSize != 16) { + return false + } + first := t.Field(0) + switch first.Type.Kind() { + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Int8, reflect.Int16, reflect.Int32: + firstKind := first.Type.Kind() + for i := 0; i < t.NumField(); i++ { + if t.Field(i).Type.Kind() != firstKind { + return false + } + } + return true + case reflect.Array: + switch first.Type.Elem().Kind() { + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Int8, reflect.Int16, reflect.Int32: + return true + default: + return false + } + default: + return false + } +} diff --git a/vendor/github.com/ebitengine/purego/struct_loong64.go b/vendor/github.com/ebitengine/purego/struct_loong64.go new file mode 100644 index 0000000..69f954f --- /dev/null +++ b/vendor/github.com/ebitengine/purego/struct_loong64.go @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 The Ebitengine Authors + +package purego + +import ( + "math" + "reflect" + "unsafe" +) + +func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) { + outSize := outType.Size() + switch { + case outSize == 0: + return reflect.New(outType).Elem() + case outSize <= 8: + r1 := syscall.a1 + if isAllFloats, numFields := isAllSameFloat(outType); isAllFloats { + r1 = syscall.f1 + if numFields == 2 { + r1 = syscall.f2<<32 | syscall.f1 + } + } + return reflect.NewAt(outType, unsafe.Pointer(&struct{ a uintptr }{r1})).Elem() + case outSize <= 16: + r1, r2 := syscall.a1, syscall.a2 + if isAllFloats, numFields := isAllSameFloat(outType); isAllFloats { + switch numFields { + case 4: + r1 = syscall.f2<<32 | syscall.f1 + r2 = syscall.f4<<32 | syscall.f3 + case 3: + r1 = syscall.f2<<32 | syscall.f1 + r2 = syscall.f3 + case 2: + r1 = syscall.f1 + r2 = syscall.f2 + default: + panic("unreachable") + } + } + return reflect.NewAt(outType, unsafe.Pointer(&struct{ a, b uintptr }{r1, r2})).Elem() + default: + // create struct from the Go pointer created above + // weird pointer dereference to circumvent go vet + return reflect.NewAt(outType, *(*unsafe.Pointer)(unsafe.Pointer(&syscall.a1))).Elem() + } +} + +const ( + _NO_CLASS = 0b00 + _FLOAT = 0b01 + _INT = 0b11 +) + +func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFloat, addStack func(uintptr), keepAlive []any) []any { + if v.Type().Size() == 0 { + return keepAlive + } + + if size := v.Type().Size(); size <= 16 { + placeRegisters(v, addFloat, addInt) + } else { + keepAlive = placeStack(v, keepAlive, addInt) + } + return keepAlive // the struct was allocated so don't panic +} + +func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) { + var val uint64 + var shift byte + var flushed bool + class := _NO_CLASS + var place func(v reflect.Value) + place = func(v reflect.Value) { + var numFields int + if v.Kind() == reflect.Struct { + numFields = v.Type().NumField() + } else { + numFields = v.Type().Len() + } + for k := 0; k < numFields; k++ { + flushed = false + var f reflect.Value + if v.Kind() == reflect.Struct { + f = v.Field(k) + } else { + f = v.Index(k) + } + align := byte(f.Type().Align()*8 - 1) + shift = (shift + align) &^ align + if shift >= 64 { + shift = 0 + flushed = true + if class == _FLOAT { + addFloat(uintptr(val)) + } else { + addInt(uintptr(val)) + } + } + switch f.Type().Kind() { + case reflect.Struct: + place(f) + case reflect.Bool: + if f.Bool() { + val |= 1 + } + shift += 8 + class |= _INT + case reflect.Uint8: + val |= f.Uint() << shift + shift += 8 + class |= _INT + case reflect.Uint16: + val |= f.Uint() << shift + shift += 16 + class |= _INT + case reflect.Uint32: + val |= f.Uint() << shift + shift += 32 + class |= _INT + case reflect.Uint64, reflect.Uint, reflect.Uintptr: + addInt(uintptr(f.Uint())) + shift = 0 + flushed = true + class = _NO_CLASS + case reflect.Int8: + val |= uint64(f.Int()&0xFF) << shift + shift += 8 + class |= _INT + case reflect.Int16: + val |= uint64(f.Int()&0xFFFF) << shift + shift += 16 + class |= _INT + case reflect.Int32: + val |= uint64(f.Int()&0xFFFF_FFFF) << shift + shift += 32 + class |= _INT + case reflect.Int64, reflect.Int: + addInt(uintptr(f.Int())) + shift = 0 + flushed = true + class = _NO_CLASS + case reflect.Float32: + if class == _FLOAT { + addFloat(uintptr(val)) + val = 0 + shift = 0 + } + val |= uint64(math.Float32bits(float32(f.Float()))) << shift + shift += 32 + class |= _FLOAT + case reflect.Float64: + addFloat(uintptr(math.Float64bits(float64(f.Float())))) + shift = 0 + flushed = true + class = _NO_CLASS + case reflect.Ptr: + addInt(f.Pointer()) + shift = 0 + flushed = true + class = _NO_CLASS + case reflect.Array: + place(f) + default: + panic("purego: unsupported kind " + f.Kind().String()) + } + } + } + place(v) + if !flushed { + if class == _FLOAT { + addFloat(uintptr(val)) + } else { + addInt(uintptr(val)) + } + } +} + +func placeStack(v reflect.Value, keepAlive []any, addInt func(uintptr)) []any { + // Struct is too big to be placed in registers. + // Copy to heap and place the pointer in register + ptrStruct := reflect.New(v.Type()) + ptrStruct.Elem().Set(v) + ptr := ptrStruct.Elem().Addr().UnsafePointer() + keepAlive = append(keepAlive, ptr) + addInt(uintptr(ptr)) + return keepAlive +} diff --git a/vendor/github.com/ebitengine/purego/struct_other.go b/vendor/github.com/ebitengine/purego/struct_other.go new file mode 100644 index 0000000..58ccc97 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/struct_other.go @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2024 The Ebitengine Authors + +//go:build !amd64 && !arm64 && !loong64 + +package purego + +import "reflect" + +func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFloat, addStack func(uintptr), keepAlive []any) []any { + panic("purego: struct arguments are not supported") +} + +func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) { + panic("purego: struct returns are not supported") +} + +func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr)) { + panic("purego: not needed on other platforms") +} diff --git a/vendor/github.com/ebitengine/purego/sys_amd64.s b/vendor/github.com/ebitengine/purego/sys_amd64.s new file mode 100644 index 0000000..a364dd0 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/sys_amd64.s @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2022 The Ebitengine Authors + +//go:build darwin || freebsd || linux || netbsd + +#include "textflag.h" +#include "abi_amd64.h" +#include "go_asm.h" +#include "funcdata.h" + +#define STACK_SIZE 80 +#define PTR_ADDRESS (STACK_SIZE - 8) + +// syscall15X calls a function in libc on behalf of the syscall package. +// syscall15X takes a pointer to a struct like: +// struct { +// fn uintptr +// a1 uintptr +// a2 uintptr +// a3 uintptr +// a4 uintptr +// a5 uintptr +// a6 uintptr +// a7 uintptr +// a8 uintptr +// a9 uintptr +// a10 uintptr +// a11 uintptr +// a12 uintptr +// a13 uintptr +// a14 uintptr +// a15 uintptr +// r1 uintptr +// r2 uintptr +// err uintptr +// } +// syscall15X must be called on the g0 stack with the +// C calling convention (use libcCall). +GLOBL ·syscall15XABI0(SB), NOPTR|RODATA, $8 +DATA ·syscall15XABI0(SB)/8, $syscall15X(SB) +TEXT syscall15X(SB), NOSPLIT|NOFRAME, $0 + PUSHQ BP + MOVQ SP, BP + SUBQ $STACK_SIZE, SP + MOVQ DI, PTR_ADDRESS(BP) // save the pointer + MOVQ DI, R11 + + MOVQ syscall15Args_f1(R11), X0 // f1 + MOVQ syscall15Args_f2(R11), X1 // f2 + MOVQ syscall15Args_f3(R11), X2 // f3 + MOVQ syscall15Args_f4(R11), X3 // f4 + MOVQ syscall15Args_f5(R11), X4 // f5 + MOVQ syscall15Args_f6(R11), X5 // f6 + MOVQ syscall15Args_f7(R11), X6 // f7 + MOVQ syscall15Args_f8(R11), X7 // f8 + + MOVQ syscall15Args_a1(R11), DI // a1 + MOVQ syscall15Args_a2(R11), SI // a2 + MOVQ syscall15Args_a3(R11), DX // a3 + MOVQ syscall15Args_a4(R11), CX // a4 + MOVQ syscall15Args_a5(R11), R8 // a5 + MOVQ syscall15Args_a6(R11), R9 // a6 + + // push the remaining paramters onto the stack + MOVQ syscall15Args_a7(R11), R12 + MOVQ R12, 0(SP) // push a7 + MOVQ syscall15Args_a8(R11), R12 + MOVQ R12, 8(SP) // push a8 + MOVQ syscall15Args_a9(R11), R12 + MOVQ R12, 16(SP) // push a9 + MOVQ syscall15Args_a10(R11), R12 + MOVQ R12, 24(SP) // push a10 + MOVQ syscall15Args_a11(R11), R12 + MOVQ R12, 32(SP) // push a11 + MOVQ syscall15Args_a12(R11), R12 + MOVQ R12, 40(SP) // push a12 + MOVQ syscall15Args_a13(R11), R12 + MOVQ R12, 48(SP) // push a13 + MOVQ syscall15Args_a14(R11), R12 + MOVQ R12, 56(SP) // push a14 + MOVQ syscall15Args_a15(R11), R12 + MOVQ R12, 64(SP) // push a15 + XORL AX, AX // vararg: say "no float args" + + MOVQ syscall15Args_fn(R11), R10 // fn + CALL R10 + + MOVQ PTR_ADDRESS(BP), DI // get the pointer back + MOVQ AX, syscall15Args_a1(DI) // r1 + MOVQ DX, syscall15Args_a2(DI) // r3 + MOVQ X0, syscall15Args_f1(DI) // f1 + MOVQ X1, syscall15Args_f2(DI) // f2 + + XORL AX, AX // no error (it's ignored anyway) + ADDQ $STACK_SIZE, SP + MOVQ BP, SP + POPQ BP + RET + +TEXT callbackasm1(SB), NOSPLIT|NOFRAME, $0 + MOVQ 0(SP), AX // save the return address to calculate the cb index + MOVQ 8(SP), R10 // get the return SP so that we can align register args with stack args + ADDQ $8, SP // remove return address from stack, we are not returning to callbackasm, but to its caller. + + // make space for first six int and 8 float arguments below the frame + ADJSP $14*8, SP + MOVSD X0, (1*8)(SP) + MOVSD X1, (2*8)(SP) + MOVSD X2, (3*8)(SP) + MOVSD X3, (4*8)(SP) + MOVSD X4, (5*8)(SP) + MOVSD X5, (6*8)(SP) + MOVSD X6, (7*8)(SP) + MOVSD X7, (8*8)(SP) + MOVQ DI, (9*8)(SP) + MOVQ SI, (10*8)(SP) + MOVQ DX, (11*8)(SP) + MOVQ CX, (12*8)(SP) + MOVQ R8, (13*8)(SP) + MOVQ R9, (14*8)(SP) + LEAQ 8(SP), R8 // R8 = address of args vector + + PUSHQ R10 // push the stack pointer below registers + + // Switch from the host ABI to the Go ABI. + PUSH_REGS_HOST_TO_ABI0() + + // determine index into runtime·cbs table + MOVQ $callbackasm(SB), DX + SUBQ DX, AX + MOVQ $0, DX + MOVQ $5, CX // divide by 5 because each call instruction in ·callbacks is 5 bytes long + DIVL CX + SUBQ $1, AX // subtract 1 because return PC is to the next slot + + // Create a struct callbackArgs on our stack to be passed as + // the "frame" to cgocallback and on to callbackWrap. + // $24 to make enough room for the arguments to runtime.cgocallback + SUBQ $(24+callbackArgs__size), SP + MOVQ AX, (24+callbackArgs_index)(SP) // callback index + MOVQ R8, (24+callbackArgs_args)(SP) // address of args vector + MOVQ $0, (24+callbackArgs_result)(SP) // result + LEAQ 24(SP), AX // take the address of callbackArgs + + // Call cgocallback, which will call callbackWrap(frame). + MOVQ ·callbackWrap_call(SB), DI // Get the ABIInternal function pointer + MOVQ (DI), DI // without by using a closure. + MOVQ AX, SI // frame (address of callbackArgs) + MOVQ $0, CX // context + + CALL crosscall2(SB) // runtime.cgocallback(fn, frame, ctxt uintptr) + + // Get callback result. + MOVQ (24+callbackArgs_result)(SP), AX + ADDQ $(24+callbackArgs__size), SP // remove callbackArgs struct + + POP_REGS_HOST_TO_ABI0() + + POPQ R10 // get the SP back + ADJSP $-14*8, SP // remove arguments + + MOVQ R10, 0(SP) + + RET diff --git a/vendor/github.com/ebitengine/purego/sys_arm64.s b/vendor/github.com/ebitengine/purego/sys_arm64.s new file mode 100644 index 0000000..a4f5be7 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/sys_arm64.s @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2022 The Ebitengine Authors + +//go:build darwin || freebsd || linux || netbsd || windows + +#include "textflag.h" +#include "go_asm.h" +#include "funcdata.h" + +#define STACK_SIZE 64 +#define PTR_ADDRESS (STACK_SIZE - 8) + +// syscall15X calls a function in libc on behalf of the syscall package. +// syscall15X takes a pointer to a struct like: +// struct { +// fn uintptr +// a1 uintptr +// a2 uintptr +// a3 uintptr +// a4 uintptr +// a5 uintptr +// a6 uintptr +// a7 uintptr +// a8 uintptr +// a9 uintptr +// a10 uintptr +// a11 uintptr +// a12 uintptr +// a13 uintptr +// a14 uintptr +// a15 uintptr +// r1 uintptr +// r2 uintptr +// err uintptr +// } +// syscall15X must be called on the g0 stack with the +// C calling convention (use libcCall). +GLOBL ·syscall15XABI0(SB), NOPTR|RODATA, $8 +DATA ·syscall15XABI0(SB)/8, $syscall15X(SB) +TEXT syscall15X(SB), NOSPLIT, $0 + SUB $STACK_SIZE, RSP // push structure pointer + MOVD R0, PTR_ADDRESS(RSP) + MOVD R0, R9 + + FMOVD syscall15Args_f1(R9), F0 // f1 + FMOVD syscall15Args_f2(R9), F1 // f2 + FMOVD syscall15Args_f3(R9), F2 // f3 + FMOVD syscall15Args_f4(R9), F3 // f4 + FMOVD syscall15Args_f5(R9), F4 // f5 + FMOVD syscall15Args_f6(R9), F5 // f6 + FMOVD syscall15Args_f7(R9), F6 // f7 + FMOVD syscall15Args_f8(R9), F7 // f8 + + MOVD syscall15Args_a1(R9), R0 // a1 + MOVD syscall15Args_a2(R9), R1 // a2 + MOVD syscall15Args_a3(R9), R2 // a3 + MOVD syscall15Args_a4(R9), R3 // a4 + MOVD syscall15Args_a5(R9), R4 // a5 + MOVD syscall15Args_a6(R9), R5 // a6 + MOVD syscall15Args_a7(R9), R6 // a7 + MOVD syscall15Args_a8(R9), R7 // a8 + MOVD syscall15Args_arm64_r8(R9), R8 // r8 + + MOVD syscall15Args_a9(R9), R10 + MOVD R10, 0(RSP) // push a9 onto stack + MOVD syscall15Args_a10(R9), R10 + MOVD R10, 8(RSP) // push a10 onto stack + MOVD syscall15Args_a11(R9), R10 + MOVD R10, 16(RSP) // push a11 onto stack + MOVD syscall15Args_a12(R9), R10 + MOVD R10, 24(RSP) // push a12 onto stack + MOVD syscall15Args_a13(R9), R10 + MOVD R10, 32(RSP) // push a13 onto stack + MOVD syscall15Args_a14(R9), R10 + MOVD R10, 40(RSP) // push a14 onto stack + MOVD syscall15Args_a15(R9), R10 + MOVD R10, 48(RSP) // push a15 onto stack + + MOVD syscall15Args_fn(R9), R10 // fn + BL (R10) + + MOVD PTR_ADDRESS(RSP), R2 // pop structure pointer + ADD $STACK_SIZE, RSP + + MOVD R0, syscall15Args_a1(R2) // save r1 + MOVD R1, syscall15Args_a2(R2) // save r3 + FMOVD F0, syscall15Args_f1(R2) // save f0 + FMOVD F1, syscall15Args_f2(R2) // save f1 + FMOVD F2, syscall15Args_f3(R2) // save f2 + FMOVD F3, syscall15Args_f4(R2) // save f3 + + RET diff --git a/vendor/github.com/ebitengine/purego/sys_loong64.s b/vendor/github.com/ebitengine/purego/sys_loong64.s new file mode 100644 index 0000000..0f34eae --- /dev/null +++ b/vendor/github.com/ebitengine/purego/sys_loong64.s @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 The Ebitengine Authors + +//go:build linux + +#include "textflag.h" +#include "go_asm.h" +#include "funcdata.h" + +#define STACK_SIZE 64 +#define PTR_ADDRESS (STACK_SIZE - 8) + +// syscall15X calls a function in libc on behalf of the syscall package. +// syscall15X takes a pointer to a struct like: +// struct { +// fn uintptr +// a1 uintptr +// a2 uintptr +// a3 uintptr +// a4 uintptr +// a5 uintptr +// a6 uintptr +// a7 uintptr +// a8 uintptr +// a9 uintptr +// a10 uintptr +// a11 uintptr +// a12 uintptr +// a13 uintptr +// a14 uintptr +// a15 uintptr +// r1 uintptr +// r2 uintptr +// err uintptr +// } +// syscall15X must be called on the g0 stack with the +// C calling convention (use libcCall). +GLOBL ·syscall15XABI0(SB), NOPTR|RODATA, $8 +DATA ·syscall15XABI0(SB)/8, $syscall15X(SB) +TEXT syscall15X(SB), NOSPLIT, $0 + // push structure pointer + SUBV $STACK_SIZE, R3 + MOVV R4, PTR_ADDRESS(R3) + MOVV R4, R13 + + MOVD syscall15Args_f1(R13), F0 // f1 + MOVD syscall15Args_f2(R13), F1 // f2 + MOVD syscall15Args_f3(R13), F2 // f3 + MOVD syscall15Args_f4(R13), F3 // f4 + MOVD syscall15Args_f5(R13), F4 // f5 + MOVD syscall15Args_f6(R13), F5 // f6 + MOVD syscall15Args_f7(R13), F6 // f7 + MOVD syscall15Args_f8(R13), F7 // f8 + + MOVV syscall15Args_a1(R13), R4 // a1 + MOVV syscall15Args_a2(R13), R5 // a2 + MOVV syscall15Args_a3(R13), R6 // a3 + MOVV syscall15Args_a4(R13), R7 // a4 + MOVV syscall15Args_a5(R13), R8 // a5 + MOVV syscall15Args_a6(R13), R9 // a6 + MOVV syscall15Args_a7(R13), R10 // a7 + MOVV syscall15Args_a8(R13), R11 // a8 + + // push a9-a15 onto stack + MOVV syscall15Args_a9(R13), R12 + MOVV R12, 0(R3) + MOVV syscall15Args_a10(R13), R12 + MOVV R12, 8(R3) + MOVV syscall15Args_a11(R13), R12 + MOVV R12, 16(R3) + MOVV syscall15Args_a12(R13), R12 + MOVV R12, 24(R3) + MOVV syscall15Args_a13(R13), R12 + MOVV R12, 32(R3) + MOVV syscall15Args_a14(R13), R12 + MOVV R12, 40(R3) + MOVV syscall15Args_a15(R13), R12 + MOVV R12, 48(R3) + + MOVV syscall15Args_fn(R13), R12 + JAL (R12) + + // pop structure pointer + MOVV PTR_ADDRESS(R3), R13 + ADDV $STACK_SIZE, R3 + + // save R4, R5 + MOVV R4, syscall15Args_a1(R13) + MOVV R5, syscall15Args_a2(R13) + + // save f0-f3 + MOVD F0, syscall15Args_f1(R13) + MOVD F1, syscall15Args_f2(R13) + MOVD F2, syscall15Args_f3(R13) + MOVD F3, syscall15Args_f4(R13) + RET diff --git a/vendor/github.com/ebitengine/purego/sys_unix_arm64.s b/vendor/github.com/ebitengine/purego/sys_unix_arm64.s new file mode 100644 index 0000000..cea803e --- /dev/null +++ b/vendor/github.com/ebitengine/purego/sys_unix_arm64.s @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 The Ebitengine Authors + +//go:build darwin || freebsd || linux || netbsd + +#include "textflag.h" +#include "go_asm.h" +#include "funcdata.h" +#include "abi_arm64.h" + +TEXT callbackasm1(SB), NOSPLIT|NOFRAME, $0 + NO_LOCAL_POINTERS + + // On entry, the trampoline in zcallback_darwin_arm64.s left + // the callback index in R12 (which is volatile in the C ABI). + + // Save callback register arguments R0-R7 and F0-F7. + // We do this at the top of the frame so they're contiguous with stack arguments. + SUB $(16*8), RSP, R14 + FSTPD (F0, F1), (0*8)(R14) + FSTPD (F2, F3), (2*8)(R14) + FSTPD (F4, F5), (4*8)(R14) + FSTPD (F6, F7), (6*8)(R14) + STP (R0, R1), (8*8)(R14) + STP (R2, R3), (10*8)(R14) + STP (R4, R5), (12*8)(R14) + STP (R6, R7), (14*8)(R14) + + // Adjust SP by frame size. + SUB $(26*8), RSP + + // It is important to save R27 because the go assembler + // uses it for move instructions for a variable. + // This line: + // MOVD ·callbackWrap_call(SB), R0 + // Creates the instructions: + // ADRP 14335(PC), R27 + // MOVD 388(27), R0 + // R27 is a callee saved register so we are responsible + // for ensuring its value doesn't change. So save it and + // restore it at the end of this function. + // R30 is the link register. crosscall2 doesn't save it + // so it's saved here. + STP (R27, R30), 0(RSP) + + // Create a struct callbackArgs on our stack. + MOVD $(callbackArgs__size)(RSP), R13 + MOVD R12, callbackArgs_index(R13) // callback index + MOVD R14, callbackArgs_args(R13) // address of args vector + MOVD ZR, callbackArgs_result(R13) // result + + // Move parameters into registers + // Get the ABIInternal function pointer + // without by using a closure. + MOVD ·callbackWrap_call(SB), R0 + MOVD (R0), R0 // fn unsafe.Pointer + MOVD R13, R1 // frame (&callbackArgs{...}) + MOVD $0, R3 // ctxt uintptr + + BL crosscall2(SB) + + // Get callback result. + MOVD $(callbackArgs__size)(RSP), R13 + MOVD callbackArgs_result(R13), R0 + + // Restore LR and R27 + LDP 0(RSP), (R27, R30) + ADD $(26*8), RSP + + RET diff --git a/vendor/github.com/ebitengine/purego/sys_unix_loong64.s b/vendor/github.com/ebitengine/purego/sys_unix_loong64.s new file mode 100644 index 0000000..89dbd7d --- /dev/null +++ b/vendor/github.com/ebitengine/purego/sys_unix_loong64.s @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 The Ebitengine Authors + +//go:build linux + +#include "textflag.h" +#include "go_asm.h" +#include "funcdata.h" +#include "abi_loong64.h" + +TEXT callbackasm1(SB), NOSPLIT|NOFRAME, $0 + NO_LOCAL_POINTERS + + SUBV $(16*8), R3, R14 + MOVD F0, 0(R14) + MOVD F1, 8(R14) + MOVD F2, 16(R14) + MOVD F3, 24(R14) + MOVD F4, 32(R14) + MOVD F5, 40(R14) + MOVD F6, 48(R14) + MOVD F7, 56(R14) + MOVV R4, 64(R14) + MOVV R5, 72(R14) + MOVV R6, 80(R14) + MOVV R7, 88(R14) + MOVV R8, 96(R14) + MOVV R9, 104(R14) + MOVV R10, 112(R14) + MOVV R11, 120(R14) + + // Adjust SP by frame size. + SUBV $(22*8), R3 + + // It is important to save R30 because the go assembler + // uses it for move instructions for a variable. + // This line: + // MOVV ·callbackWrap_call(SB), R4 + // Creates the instructions: + // PCALAU12I off1(PC), R30 + // MOVV off2(R30), R4 + // R30 is a callee saved register so we are responsible + // for ensuring its value doesn't change. So save it and + // restore it at the end of this function. + // R1 is the link register. crosscall2 doesn't save it + // so it's saved here. + MOVV R1, 0(R3) + MOVV R30, 8(R3) + + // Create a struct callbackArgs on our stack. + MOVV $(callbackArgs__size)(R3), R13 + MOVV R12, callbackArgs_index(R13) // callback index + MOVV R14, callbackArgs_args(R13) // address of args vector + MOVV $0, callbackArgs_result(R13) // result + + // Move parameters into registers + // Get the ABIInternal function pointer + // without by using a closure. + MOVV ·callbackWrap_call(SB), R4 + MOVV (R4), R4 // fn unsafe.Pointer + MOVV R13, R5 // frame (&callbackArgs{...}) + MOVV $0, R7 // ctxt uintptr + + JAL crosscall2(SB) + + // Get callback result. + MOVV $(callbackArgs__size)(R3), R13 + MOVV callbackArgs_result(R13), R4 + + // Restore LR and R30 + MOVV 0(R3), R1 + MOVV 8(R3), R30 + ADDV $(22*8), R3 + + RET diff --git a/vendor/github.com/ebitengine/purego/syscall.go b/vendor/github.com/ebitengine/purego/syscall.go new file mode 100644 index 0000000..ccfc498 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/syscall.go @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2022 The Ebitengine Authors + +//go:build darwin || freebsd || linux || netbsd || windows + +package purego + +// CDecl marks a function as being called using the __cdecl calling convention as defined in +// the [MSDocs] when passed to NewCallback. It must be the first argument to the function. +// This is only useful on 386 Windows, but it is safe to use on other platforms. +// +// [MSDocs]: https://learn.microsoft.com/en-us/cpp/cpp/cdecl?view=msvc-170 +type CDecl struct{} + +const ( + maxArgs = 15 + numOfFloatRegisters = 8 // arm64 and amd64 both have 8 float registers +) + +type syscall15Args struct { + fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 uintptr + f1, f2, f3, f4, f5, f6, f7, f8 uintptr + arm64_r8 uintptr +} + +// SyscallN takes fn, a C function pointer and a list of arguments as uintptr. +// There is an internal maximum number of arguments that SyscallN can take. It panics +// when the maximum is exceeded. It returns the result and the libc error code if there is one. +// +// In order to call this function properly make sure to follow all the rules specified in [unsafe.Pointer] +// especially point 4. +// +// NOTE: SyscallN does not properly call functions that have both integer and float parameters. +// See discussion comment https://github.com/ebiten/purego/pull/1#issuecomment-1128057607 +// for an explanation of why that is. +// +// On amd64, if there are more than 8 floats the 9th and so on will be placed incorrectly on the +// stack. +// +// The pragma go:nosplit is not needed at this function declaration because it uses go:uintptrescapes +// which forces all the objects that the uintptrs point to onto the heap where a stack split won't affect +// their memory location. +// +//go:uintptrescapes +func SyscallN(fn uintptr, args ...uintptr) (r1, r2, err uintptr) { + if fn == 0 { + panic("purego: fn is nil") + } + if len(args) > maxArgs { + panic("purego: too many arguments to SyscallN") + } + // add padding so there is no out-of-bounds slicing + var tmp [maxArgs]uintptr + copy(tmp[:], args) + return syscall_syscall15X(fn, tmp[0], tmp[1], tmp[2], tmp[3], tmp[4], tmp[5], tmp[6], tmp[7], tmp[8], tmp[9], tmp[10], tmp[11], tmp[12], tmp[13], tmp[14]) +} diff --git a/vendor/github.com/ebitengine/purego/syscall_cgo_linux.go b/vendor/github.com/ebitengine/purego/syscall_cgo_linux.go new file mode 100644 index 0000000..7794c26 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/syscall_cgo_linux.go @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2022 The Ebitengine Authors + +//go:build cgo && !(amd64 || arm64 || loong64) + +package purego + +import ( + "github.com/ebitengine/purego/internal/cgo" +) + +var syscall15XABI0 = uintptr(cgo.Syscall15XABI0) + +//go:nosplit +func syscall_syscall15X(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 uintptr) (r1, r2, err uintptr) { + return cgo.Syscall15X(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15) +} + +func NewCallback(_ any) uintptr { + panic("purego: NewCallback on Linux is only supported on amd64/arm64/loong64") +} diff --git a/vendor/github.com/ebitengine/purego/syscall_sysv.go b/vendor/github.com/ebitengine/purego/syscall_sysv.go new file mode 100644 index 0000000..d794bc3 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/syscall_sysv.go @@ -0,0 +1,226 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2022 The Ebitengine Authors + +//go:build darwin || freebsd || (linux && (amd64 || arm64 || loong64)) || netbsd + +package purego + +import ( + "reflect" + "runtime" + "sync" + "unsafe" +) + +var syscall15XABI0 uintptr + +func syscall_syscall15X(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 uintptr) (r1, r2, err uintptr) { + args := thePool.Get().(*syscall15Args) + defer thePool.Put(args) + + *args = syscall15Args{ + fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, + a1, a2, a3, a4, a5, a6, a7, a8, + 0, + } + + runtime_cgocall(syscall15XABI0, unsafe.Pointer(args)) + return args.a1, args.a2, 0 +} + +// NewCallback converts a Go function to a function pointer conforming to the C calling convention. +// This is useful when interoperating with C code requiring callbacks. The argument is expected to be a +// function with zero or one uintptr-sized result. The function must not have arguments with size larger than the size +// of uintptr. Only a limited number of callbacks may be created in a single Go process, and any memory allocated +// for these callbacks is never released. At least 2000 callbacks can always be created. Although this function +// provides similar functionality to windows.NewCallback it is distinct. +func NewCallback(fn any) uintptr { + ty := reflect.TypeOf(fn) + for i := 0; i < ty.NumIn(); i++ { + in := ty.In(i) + if !in.AssignableTo(reflect.TypeOf(CDecl{})) { + continue + } + if i != 0 { + panic("purego: CDecl must be the first argument") + } + } + return compileCallback(fn) +} + +// maxCb is the maximum number of callbacks +// only increase this if you have added more to the callbackasm function +const maxCB = 2000 + +var cbs struct { + lock sync.Mutex + numFn int // the number of functions currently in cbs.funcs + funcs [maxCB]reflect.Value // the saved callbacks +} + +type callbackArgs struct { + index uintptr + // args points to the argument block. + // + // The structure of the arguments goes + // float registers followed by the + // integer registers followed by the stack. + // + // This variable is treated as a continuous + // block of memory containing all of the arguments + // for this callback. + args unsafe.Pointer + // Below are out-args from callbackWrap + result uintptr +} + +func compileCallback(fn any) uintptr { + val := reflect.ValueOf(fn) + if val.Kind() != reflect.Func { + panic("purego: the type must be a function but was not") + } + if val.IsNil() { + panic("purego: function must not be nil") + } + ty := val.Type() + for i := 0; i < ty.NumIn(); i++ { + in := ty.In(i) + switch in.Kind() { + case reflect.Struct: + if i == 0 && in.AssignableTo(reflect.TypeOf(CDecl{})) { + continue + } + fallthrough + case reflect.Interface, reflect.Func, reflect.Slice, + reflect.Chan, reflect.Complex64, reflect.Complex128, + reflect.String, reflect.Map, reflect.Invalid: + panic("purego: unsupported argument type: " + in.Kind().String()) + } + } +output: + switch { + case ty.NumOut() == 1: + switch ty.Out(0).Kind() { + case reflect.Pointer, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, + reflect.Bool, reflect.UnsafePointer: + break output + } + panic("purego: unsupported return type: " + ty.String()) + case ty.NumOut() > 1: + panic("purego: callbacks can only have one return") + } + cbs.lock.Lock() + defer cbs.lock.Unlock() + if cbs.numFn >= maxCB { + panic("purego: the maximum number of callbacks has been reached") + } + cbs.funcs[cbs.numFn] = val + cbs.numFn++ + return callbackasmAddr(cbs.numFn - 1) +} + +const ptrSize = unsafe.Sizeof((*int)(nil)) + +const callbackMaxFrame = 64 * ptrSize + +// callbackasm is implemented in zcallback_GOOS_GOARCH.s +// +//go:linkname __callbackasm callbackasm +var __callbackasm byte +var callbackasmABI0 = uintptr(unsafe.Pointer(&__callbackasm)) + +// callbackWrap_call allows the calling of the ABIInternal wrapper +// which is required for runtime.cgocallback without the +// tag which is only allowed in the runtime. +// This closure is used inside sys_darwin_GOARCH.s +var callbackWrap_call = callbackWrap + +// callbackWrap is called by assembly code which determines which Go function to call. +// This function takes the arguments and passes them to the Go function and returns the result. +func callbackWrap(a *callbackArgs) { + cbs.lock.Lock() + fn := cbs.funcs[a.index] + cbs.lock.Unlock() + fnType := fn.Type() + args := make([]reflect.Value, fnType.NumIn()) + frame := (*[callbackMaxFrame]uintptr)(a.args) + var floatsN int // floatsN represents the number of float arguments processed + var intsN int // intsN represents the number of integer arguments processed + // stack points to the index into frame of the current stack element. + // The stack begins after the float and integer registers. + stack := numOfIntegerRegisters() + numOfFloatRegisters + for i := range args { + var pos int + switch fnType.In(i).Kind() { + case reflect.Float32, reflect.Float64: + if floatsN >= numOfFloatRegisters { + pos = stack + stack++ + } else { + pos = floatsN + } + floatsN++ + case reflect.Struct: + // This is the CDecl field + args[i] = reflect.Zero(fnType.In(i)) + continue + default: + + if intsN >= numOfIntegerRegisters() { + pos = stack + stack++ + } else { + // the integers begin after the floats in frame + pos = intsN + numOfFloatRegisters + } + intsN++ + } + args[i] = reflect.NewAt(fnType.In(i), unsafe.Pointer(&frame[pos])).Elem() + } + ret := fn.Call(args) + if len(ret) > 0 { + switch k := ret[0].Kind(); k { + case reflect.Uint, reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8, reflect.Uintptr: + a.result = uintptr(ret[0].Uint()) + case reflect.Int, reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8: + a.result = uintptr(ret[0].Int()) + case reflect.Bool: + if ret[0].Bool() { + a.result = 1 + } else { + a.result = 0 + } + case reflect.Pointer: + a.result = ret[0].Pointer() + case reflect.UnsafePointer: + a.result = ret[0].Pointer() + default: + panic("purego: unsupported kind: " + k.String()) + } + } +} + +// callbackasmAddr returns address of runtime.callbackasm +// function adjusted by i. +// On x86 and amd64, runtime.callbackasm is a series of CALL instructions, +// and we want callback to arrive at +// correspondent call instruction instead of start of +// runtime.callbackasm. +// On ARM, runtime.callbackasm is a series of mov and branch instructions. +// R12 is loaded with the callback index. Each entry is two instructions, +// hence 8 bytes. +func callbackasmAddr(i int) uintptr { + var entrySize int + switch runtime.GOARCH { + default: + panic("purego: unsupported architecture") + case "386", "amd64": + entrySize = 5 + case "arm", "arm64", "loong64": + // On ARM and ARM64, each entry is a MOV instruction + // followed by a branch instruction + entrySize = 8 + } + return callbackasmABI0 + uintptr(i*entrySize) +} diff --git a/vendor/github.com/ebitengine/purego/syscall_windows.go b/vendor/github.com/ebitengine/purego/syscall_windows.go new file mode 100644 index 0000000..5afd8d8 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/syscall_windows.go @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2022 The Ebitengine Authors + +package purego + +import ( + "reflect" + "syscall" +) + +var syscall15XABI0 uintptr + +func syscall_syscall15X(fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 uintptr) (r1, r2, err uintptr) { + r1, r2, errno := syscall.Syscall15(fn, 15, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15) + return r1, r2, uintptr(errno) +} + +// NewCallback converts a Go function to a function pointer conforming to the stdcall calling convention. +// This is useful when interoperating with Windows code requiring callbacks. The argument is expected to be a +// function with one uintptr-sized result. The function must not have arguments with size larger than the +// size of uintptr. Only a limited number of callbacks may be created in a single Go process, and any memory +// allocated for these callbacks is never released. Between NewCallback and NewCallbackCDecl, at least 1024 +// callbacks can always be created. Although this function is similiar to the darwin version it may act +// differently. +func NewCallback(fn any) uintptr { + isCDecl := false + ty := reflect.TypeOf(fn) + for i := 0; i < ty.NumIn(); i++ { + in := ty.In(i) + if !in.AssignableTo(reflect.TypeOf(CDecl{})) { + continue + } + if i != 0 { + panic("purego: CDecl must be the first argument") + } + isCDecl = true + } + if isCDecl { + return syscall.NewCallbackCDecl(fn) + } + return syscall.NewCallback(fn) +} + +func loadSymbol(handle uintptr, name string) (uintptr, error) { + return syscall.GetProcAddress(syscall.Handle(handle), name) +} diff --git a/vendor/github.com/ebitengine/purego/zcallback_amd64.s b/vendor/github.com/ebitengine/purego/zcallback_amd64.s new file mode 100644 index 0000000..42b54c4 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/zcallback_amd64.s @@ -0,0 +1,2014 @@ +// Code generated by wincallback.go using 'go generate'. DO NOT EDIT. + +//go:build darwin || freebsd || linux || netbsd + +// runtime·callbackasm is called by external code to +// execute Go implemented callback function. It is not +// called from the start, instead runtime·compilecallback +// always returns address into runtime·callbackasm offset +// appropriately so different callbacks start with different +// CALL instruction in runtime·callbackasm. This determines +// which Go callback function is executed later on. +#include "textflag.h" + +TEXT callbackasm(SB),NOSPLIT|NOFRAME,$0 + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) + CALL callbackasm1(SB) diff --git a/vendor/github.com/ebitengine/purego/zcallback_arm64.s b/vendor/github.com/ebitengine/purego/zcallback_arm64.s new file mode 100644 index 0000000..087c2d4 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/zcallback_arm64.s @@ -0,0 +1,4014 @@ +// Code generated by wincallback.go using 'go generate'. DO NOT EDIT. + +//go:build darwin || freebsd || linux || netbsd + +// External code calls into callbackasm at an offset corresponding +// to the callback index. Callbackasm is a table of MOV and B instructions. +// The MOV instruction loads R12 with the callback index, and the +// B instruction branches to callbackasm1. +// callbackasm1 takes the callback index from R12 and +// indexes into an array that stores information about each callback. +// It then calls the Go implementation for that callback. +#include "textflag.h" + +TEXT callbackasm(SB),NOSPLIT|NOFRAME,$0 + MOVD $0, R12 + B callbackasm1(SB) + MOVD $1, R12 + B callbackasm1(SB) + MOVD $2, R12 + B callbackasm1(SB) + MOVD $3, R12 + B callbackasm1(SB) + MOVD $4, R12 + B callbackasm1(SB) + MOVD $5, R12 + B callbackasm1(SB) + MOVD $6, R12 + B callbackasm1(SB) + MOVD $7, R12 + B callbackasm1(SB) + MOVD $8, R12 + B callbackasm1(SB) + MOVD $9, R12 + B callbackasm1(SB) + MOVD $10, R12 + B callbackasm1(SB) + MOVD $11, R12 + B callbackasm1(SB) + MOVD $12, R12 + B callbackasm1(SB) + MOVD $13, R12 + B callbackasm1(SB) + MOVD $14, R12 + B callbackasm1(SB) + MOVD $15, R12 + B callbackasm1(SB) + MOVD $16, R12 + B callbackasm1(SB) + MOVD $17, R12 + B callbackasm1(SB) + MOVD $18, R12 + B callbackasm1(SB) + MOVD $19, R12 + B callbackasm1(SB) + MOVD $20, R12 + B callbackasm1(SB) + MOVD $21, R12 + B callbackasm1(SB) + MOVD $22, R12 + B callbackasm1(SB) + MOVD $23, R12 + B callbackasm1(SB) + MOVD $24, R12 + B callbackasm1(SB) + MOVD $25, R12 + B callbackasm1(SB) + MOVD $26, R12 + B callbackasm1(SB) + MOVD $27, R12 + B callbackasm1(SB) + MOVD $28, R12 + B callbackasm1(SB) + MOVD $29, R12 + B callbackasm1(SB) + MOVD $30, R12 + B callbackasm1(SB) + MOVD $31, R12 + B callbackasm1(SB) + MOVD $32, R12 + B callbackasm1(SB) + MOVD $33, R12 + B callbackasm1(SB) + MOVD $34, R12 + B callbackasm1(SB) + MOVD $35, R12 + B callbackasm1(SB) + MOVD $36, R12 + B callbackasm1(SB) + MOVD $37, R12 + B callbackasm1(SB) + MOVD $38, R12 + B callbackasm1(SB) + MOVD $39, R12 + B callbackasm1(SB) + MOVD $40, R12 + B callbackasm1(SB) + MOVD $41, R12 + B callbackasm1(SB) + MOVD $42, R12 + B callbackasm1(SB) + MOVD $43, R12 + B callbackasm1(SB) + MOVD $44, R12 + B callbackasm1(SB) + MOVD $45, R12 + B callbackasm1(SB) + MOVD $46, R12 + B callbackasm1(SB) + MOVD $47, R12 + B callbackasm1(SB) + MOVD $48, R12 + B callbackasm1(SB) + MOVD $49, R12 + B callbackasm1(SB) + MOVD $50, R12 + B callbackasm1(SB) + MOVD $51, R12 + B callbackasm1(SB) + MOVD $52, R12 + B callbackasm1(SB) + MOVD $53, R12 + B callbackasm1(SB) + MOVD $54, R12 + B callbackasm1(SB) + MOVD $55, R12 + B callbackasm1(SB) + MOVD $56, R12 + B callbackasm1(SB) + MOVD $57, R12 + B callbackasm1(SB) + MOVD $58, R12 + B callbackasm1(SB) + MOVD $59, R12 + B callbackasm1(SB) + MOVD $60, R12 + B callbackasm1(SB) + MOVD $61, R12 + B callbackasm1(SB) + MOVD $62, R12 + B callbackasm1(SB) + MOVD $63, R12 + B callbackasm1(SB) + MOVD $64, R12 + B callbackasm1(SB) + MOVD $65, R12 + B callbackasm1(SB) + MOVD $66, R12 + B callbackasm1(SB) + MOVD $67, R12 + B callbackasm1(SB) + MOVD $68, R12 + B callbackasm1(SB) + MOVD $69, R12 + B callbackasm1(SB) + MOVD $70, R12 + B callbackasm1(SB) + MOVD $71, R12 + B callbackasm1(SB) + MOVD $72, R12 + B callbackasm1(SB) + MOVD $73, R12 + B callbackasm1(SB) + MOVD $74, R12 + B callbackasm1(SB) + MOVD $75, R12 + B callbackasm1(SB) + MOVD $76, R12 + B callbackasm1(SB) + MOVD $77, R12 + B callbackasm1(SB) + MOVD $78, R12 + B callbackasm1(SB) + MOVD $79, R12 + B callbackasm1(SB) + MOVD $80, R12 + B callbackasm1(SB) + MOVD $81, R12 + B callbackasm1(SB) + MOVD $82, R12 + B callbackasm1(SB) + MOVD $83, R12 + B callbackasm1(SB) + MOVD $84, R12 + B callbackasm1(SB) + MOVD $85, R12 + B callbackasm1(SB) + MOVD $86, R12 + B callbackasm1(SB) + MOVD $87, R12 + B callbackasm1(SB) + MOVD $88, R12 + B callbackasm1(SB) + MOVD $89, R12 + B callbackasm1(SB) + MOVD $90, R12 + B callbackasm1(SB) + MOVD $91, R12 + B callbackasm1(SB) + MOVD $92, R12 + B callbackasm1(SB) + MOVD $93, R12 + B callbackasm1(SB) + MOVD $94, R12 + B callbackasm1(SB) + MOVD $95, R12 + B callbackasm1(SB) + MOVD $96, R12 + B callbackasm1(SB) + MOVD $97, R12 + B callbackasm1(SB) + MOVD $98, R12 + B callbackasm1(SB) + MOVD $99, R12 + B callbackasm1(SB) + MOVD $100, R12 + B callbackasm1(SB) + MOVD $101, R12 + B callbackasm1(SB) + MOVD $102, R12 + B callbackasm1(SB) + MOVD $103, R12 + B callbackasm1(SB) + MOVD $104, R12 + B callbackasm1(SB) + MOVD $105, R12 + B callbackasm1(SB) + MOVD $106, R12 + B callbackasm1(SB) + MOVD $107, R12 + B callbackasm1(SB) + MOVD $108, R12 + B callbackasm1(SB) + MOVD $109, R12 + B callbackasm1(SB) + MOVD $110, R12 + B callbackasm1(SB) + MOVD $111, R12 + B callbackasm1(SB) + MOVD $112, R12 + B callbackasm1(SB) + MOVD $113, R12 + B callbackasm1(SB) + MOVD $114, R12 + B callbackasm1(SB) + MOVD $115, R12 + B callbackasm1(SB) + MOVD $116, R12 + B callbackasm1(SB) + MOVD $117, R12 + B callbackasm1(SB) + MOVD $118, R12 + B callbackasm1(SB) + MOVD $119, R12 + B callbackasm1(SB) + MOVD $120, R12 + B callbackasm1(SB) + MOVD $121, R12 + B callbackasm1(SB) + MOVD $122, R12 + B callbackasm1(SB) + MOVD $123, R12 + B callbackasm1(SB) + MOVD $124, R12 + B callbackasm1(SB) + MOVD $125, R12 + B callbackasm1(SB) + MOVD $126, R12 + B callbackasm1(SB) + MOVD $127, R12 + B callbackasm1(SB) + MOVD $128, R12 + B callbackasm1(SB) + MOVD $129, R12 + B callbackasm1(SB) + MOVD $130, R12 + B callbackasm1(SB) + MOVD $131, R12 + B callbackasm1(SB) + MOVD $132, R12 + B callbackasm1(SB) + MOVD $133, R12 + B callbackasm1(SB) + MOVD $134, R12 + B callbackasm1(SB) + MOVD $135, R12 + B callbackasm1(SB) + MOVD $136, R12 + B callbackasm1(SB) + MOVD $137, R12 + B callbackasm1(SB) + MOVD $138, R12 + B callbackasm1(SB) + MOVD $139, R12 + B callbackasm1(SB) + MOVD $140, R12 + B callbackasm1(SB) + MOVD $141, R12 + B callbackasm1(SB) + MOVD $142, R12 + B callbackasm1(SB) + MOVD $143, R12 + B callbackasm1(SB) + MOVD $144, R12 + B callbackasm1(SB) + MOVD $145, R12 + B callbackasm1(SB) + MOVD $146, R12 + B callbackasm1(SB) + MOVD $147, R12 + B callbackasm1(SB) + MOVD $148, R12 + B callbackasm1(SB) + MOVD $149, R12 + B callbackasm1(SB) + MOVD $150, R12 + B callbackasm1(SB) + MOVD $151, R12 + B callbackasm1(SB) + MOVD $152, R12 + B callbackasm1(SB) + MOVD $153, R12 + B callbackasm1(SB) + MOVD $154, R12 + B callbackasm1(SB) + MOVD $155, R12 + B callbackasm1(SB) + MOVD $156, R12 + B callbackasm1(SB) + MOVD $157, R12 + B callbackasm1(SB) + MOVD $158, R12 + B callbackasm1(SB) + MOVD $159, R12 + B callbackasm1(SB) + MOVD $160, R12 + B callbackasm1(SB) + MOVD $161, R12 + B callbackasm1(SB) + MOVD $162, R12 + B callbackasm1(SB) + MOVD $163, R12 + B callbackasm1(SB) + MOVD $164, R12 + B callbackasm1(SB) + MOVD $165, R12 + B callbackasm1(SB) + MOVD $166, R12 + B callbackasm1(SB) + MOVD $167, R12 + B callbackasm1(SB) + MOVD $168, R12 + B callbackasm1(SB) + MOVD $169, R12 + B callbackasm1(SB) + MOVD $170, R12 + B callbackasm1(SB) + MOVD $171, R12 + B callbackasm1(SB) + MOVD $172, R12 + B callbackasm1(SB) + MOVD $173, R12 + B callbackasm1(SB) + MOVD $174, R12 + B callbackasm1(SB) + MOVD $175, R12 + B callbackasm1(SB) + MOVD $176, R12 + B callbackasm1(SB) + MOVD $177, R12 + B callbackasm1(SB) + MOVD $178, R12 + B callbackasm1(SB) + MOVD $179, R12 + B callbackasm1(SB) + MOVD $180, R12 + B callbackasm1(SB) + MOVD $181, R12 + B callbackasm1(SB) + MOVD $182, R12 + B callbackasm1(SB) + MOVD $183, R12 + B callbackasm1(SB) + MOVD $184, R12 + B callbackasm1(SB) + MOVD $185, R12 + B callbackasm1(SB) + MOVD $186, R12 + B callbackasm1(SB) + MOVD $187, R12 + B callbackasm1(SB) + MOVD $188, R12 + B callbackasm1(SB) + MOVD $189, R12 + B callbackasm1(SB) + MOVD $190, R12 + B callbackasm1(SB) + MOVD $191, R12 + B callbackasm1(SB) + MOVD $192, R12 + B callbackasm1(SB) + MOVD $193, R12 + B callbackasm1(SB) + MOVD $194, R12 + B callbackasm1(SB) + MOVD $195, R12 + B callbackasm1(SB) + MOVD $196, R12 + B callbackasm1(SB) + MOVD $197, R12 + B callbackasm1(SB) + MOVD $198, R12 + B callbackasm1(SB) + MOVD $199, R12 + B callbackasm1(SB) + MOVD $200, R12 + B callbackasm1(SB) + MOVD $201, R12 + B callbackasm1(SB) + MOVD $202, R12 + B callbackasm1(SB) + MOVD $203, R12 + B callbackasm1(SB) + MOVD $204, R12 + B callbackasm1(SB) + MOVD $205, R12 + B callbackasm1(SB) + MOVD $206, R12 + B callbackasm1(SB) + MOVD $207, R12 + B callbackasm1(SB) + MOVD $208, R12 + B callbackasm1(SB) + MOVD $209, R12 + B callbackasm1(SB) + MOVD $210, R12 + B callbackasm1(SB) + MOVD $211, R12 + B callbackasm1(SB) + MOVD $212, R12 + B callbackasm1(SB) + MOVD $213, R12 + B callbackasm1(SB) + MOVD $214, R12 + B callbackasm1(SB) + MOVD $215, R12 + B callbackasm1(SB) + MOVD $216, R12 + B callbackasm1(SB) + MOVD $217, R12 + B callbackasm1(SB) + MOVD $218, R12 + B callbackasm1(SB) + MOVD $219, R12 + B callbackasm1(SB) + MOVD $220, R12 + B callbackasm1(SB) + MOVD $221, R12 + B callbackasm1(SB) + MOVD $222, R12 + B callbackasm1(SB) + MOVD $223, R12 + B callbackasm1(SB) + MOVD $224, R12 + B callbackasm1(SB) + MOVD $225, R12 + B callbackasm1(SB) + MOVD $226, R12 + B callbackasm1(SB) + MOVD $227, R12 + B callbackasm1(SB) + MOVD $228, R12 + B callbackasm1(SB) + MOVD $229, R12 + B callbackasm1(SB) + MOVD $230, R12 + B callbackasm1(SB) + MOVD $231, R12 + B callbackasm1(SB) + MOVD $232, R12 + B callbackasm1(SB) + MOVD $233, R12 + B callbackasm1(SB) + MOVD $234, R12 + B callbackasm1(SB) + MOVD $235, R12 + B callbackasm1(SB) + MOVD $236, R12 + B callbackasm1(SB) + MOVD $237, R12 + B callbackasm1(SB) + MOVD $238, R12 + B callbackasm1(SB) + MOVD $239, R12 + B callbackasm1(SB) + MOVD $240, R12 + B callbackasm1(SB) + MOVD $241, R12 + B callbackasm1(SB) + MOVD $242, R12 + B callbackasm1(SB) + MOVD $243, R12 + B callbackasm1(SB) + MOVD $244, R12 + B callbackasm1(SB) + MOVD $245, R12 + B callbackasm1(SB) + MOVD $246, R12 + B callbackasm1(SB) + MOVD $247, R12 + B callbackasm1(SB) + MOVD $248, R12 + B callbackasm1(SB) + MOVD $249, R12 + B callbackasm1(SB) + MOVD $250, R12 + B callbackasm1(SB) + MOVD $251, R12 + B callbackasm1(SB) + MOVD $252, R12 + B callbackasm1(SB) + MOVD $253, R12 + B callbackasm1(SB) + MOVD $254, R12 + B callbackasm1(SB) + MOVD $255, R12 + B callbackasm1(SB) + MOVD $256, R12 + B callbackasm1(SB) + MOVD $257, R12 + B callbackasm1(SB) + MOVD $258, R12 + B callbackasm1(SB) + MOVD $259, R12 + B callbackasm1(SB) + MOVD $260, R12 + B callbackasm1(SB) + MOVD $261, R12 + B callbackasm1(SB) + MOVD $262, R12 + B callbackasm1(SB) + MOVD $263, R12 + B callbackasm1(SB) + MOVD $264, R12 + B callbackasm1(SB) + MOVD $265, R12 + B callbackasm1(SB) + MOVD $266, R12 + B callbackasm1(SB) + MOVD $267, R12 + B callbackasm1(SB) + MOVD $268, R12 + B callbackasm1(SB) + MOVD $269, R12 + B callbackasm1(SB) + MOVD $270, R12 + B callbackasm1(SB) + MOVD $271, R12 + B callbackasm1(SB) + MOVD $272, R12 + B callbackasm1(SB) + MOVD $273, R12 + B callbackasm1(SB) + MOVD $274, R12 + B callbackasm1(SB) + MOVD $275, R12 + B callbackasm1(SB) + MOVD $276, R12 + B callbackasm1(SB) + MOVD $277, R12 + B callbackasm1(SB) + MOVD $278, R12 + B callbackasm1(SB) + MOVD $279, R12 + B callbackasm1(SB) + MOVD $280, R12 + B callbackasm1(SB) + MOVD $281, R12 + B callbackasm1(SB) + MOVD $282, R12 + B callbackasm1(SB) + MOVD $283, R12 + B callbackasm1(SB) + MOVD $284, R12 + B callbackasm1(SB) + MOVD $285, R12 + B callbackasm1(SB) + MOVD $286, R12 + B callbackasm1(SB) + MOVD $287, R12 + B callbackasm1(SB) + MOVD $288, R12 + B callbackasm1(SB) + MOVD $289, R12 + B callbackasm1(SB) + MOVD $290, R12 + B callbackasm1(SB) + MOVD $291, R12 + B callbackasm1(SB) + MOVD $292, R12 + B callbackasm1(SB) + MOVD $293, R12 + B callbackasm1(SB) + MOVD $294, R12 + B callbackasm1(SB) + MOVD $295, R12 + B callbackasm1(SB) + MOVD $296, R12 + B callbackasm1(SB) + MOVD $297, R12 + B callbackasm1(SB) + MOVD $298, R12 + B callbackasm1(SB) + MOVD $299, R12 + B callbackasm1(SB) + MOVD $300, R12 + B callbackasm1(SB) + MOVD $301, R12 + B callbackasm1(SB) + MOVD $302, R12 + B callbackasm1(SB) + MOVD $303, R12 + B callbackasm1(SB) + MOVD $304, R12 + B callbackasm1(SB) + MOVD $305, R12 + B callbackasm1(SB) + MOVD $306, R12 + B callbackasm1(SB) + MOVD $307, R12 + B callbackasm1(SB) + MOVD $308, R12 + B callbackasm1(SB) + MOVD $309, R12 + B callbackasm1(SB) + MOVD $310, R12 + B callbackasm1(SB) + MOVD $311, R12 + B callbackasm1(SB) + MOVD $312, R12 + B callbackasm1(SB) + MOVD $313, R12 + B callbackasm1(SB) + MOVD $314, R12 + B callbackasm1(SB) + MOVD $315, R12 + B callbackasm1(SB) + MOVD $316, R12 + B callbackasm1(SB) + MOVD $317, R12 + B callbackasm1(SB) + MOVD $318, R12 + B callbackasm1(SB) + MOVD $319, R12 + B callbackasm1(SB) + MOVD $320, R12 + B callbackasm1(SB) + MOVD $321, R12 + B callbackasm1(SB) + MOVD $322, R12 + B callbackasm1(SB) + MOVD $323, R12 + B callbackasm1(SB) + MOVD $324, R12 + B callbackasm1(SB) + MOVD $325, R12 + B callbackasm1(SB) + MOVD $326, R12 + B callbackasm1(SB) + MOVD $327, R12 + B callbackasm1(SB) + MOVD $328, R12 + B callbackasm1(SB) + MOVD $329, R12 + B callbackasm1(SB) + MOVD $330, R12 + B callbackasm1(SB) + MOVD $331, R12 + B callbackasm1(SB) + MOVD $332, R12 + B callbackasm1(SB) + MOVD $333, R12 + B callbackasm1(SB) + MOVD $334, R12 + B callbackasm1(SB) + MOVD $335, R12 + B callbackasm1(SB) + MOVD $336, R12 + B callbackasm1(SB) + MOVD $337, R12 + B callbackasm1(SB) + MOVD $338, R12 + B callbackasm1(SB) + MOVD $339, R12 + B callbackasm1(SB) + MOVD $340, R12 + B callbackasm1(SB) + MOVD $341, R12 + B callbackasm1(SB) + MOVD $342, R12 + B callbackasm1(SB) + MOVD $343, R12 + B callbackasm1(SB) + MOVD $344, R12 + B callbackasm1(SB) + MOVD $345, R12 + B callbackasm1(SB) + MOVD $346, R12 + B callbackasm1(SB) + MOVD $347, R12 + B callbackasm1(SB) + MOVD $348, R12 + B callbackasm1(SB) + MOVD $349, R12 + B callbackasm1(SB) + MOVD $350, R12 + B callbackasm1(SB) + MOVD $351, R12 + B callbackasm1(SB) + MOVD $352, R12 + B callbackasm1(SB) + MOVD $353, R12 + B callbackasm1(SB) + MOVD $354, R12 + B callbackasm1(SB) + MOVD $355, R12 + B callbackasm1(SB) + MOVD $356, R12 + B callbackasm1(SB) + MOVD $357, R12 + B callbackasm1(SB) + MOVD $358, R12 + B callbackasm1(SB) + MOVD $359, R12 + B callbackasm1(SB) + MOVD $360, R12 + B callbackasm1(SB) + MOVD $361, R12 + B callbackasm1(SB) + MOVD $362, R12 + B callbackasm1(SB) + MOVD $363, R12 + B callbackasm1(SB) + MOVD $364, R12 + B callbackasm1(SB) + MOVD $365, R12 + B callbackasm1(SB) + MOVD $366, R12 + B callbackasm1(SB) + MOVD $367, R12 + B callbackasm1(SB) + MOVD $368, R12 + B callbackasm1(SB) + MOVD $369, R12 + B callbackasm1(SB) + MOVD $370, R12 + B callbackasm1(SB) + MOVD $371, R12 + B callbackasm1(SB) + MOVD $372, R12 + B callbackasm1(SB) + MOVD $373, R12 + B callbackasm1(SB) + MOVD $374, R12 + B callbackasm1(SB) + MOVD $375, R12 + B callbackasm1(SB) + MOVD $376, R12 + B callbackasm1(SB) + MOVD $377, R12 + B callbackasm1(SB) + MOVD $378, R12 + B callbackasm1(SB) + MOVD $379, R12 + B callbackasm1(SB) + MOVD $380, R12 + B callbackasm1(SB) + MOVD $381, R12 + B callbackasm1(SB) + MOVD $382, R12 + B callbackasm1(SB) + MOVD $383, R12 + B callbackasm1(SB) + MOVD $384, R12 + B callbackasm1(SB) + MOVD $385, R12 + B callbackasm1(SB) + MOVD $386, R12 + B callbackasm1(SB) + MOVD $387, R12 + B callbackasm1(SB) + MOVD $388, R12 + B callbackasm1(SB) + MOVD $389, R12 + B callbackasm1(SB) + MOVD $390, R12 + B callbackasm1(SB) + MOVD $391, R12 + B callbackasm1(SB) + MOVD $392, R12 + B callbackasm1(SB) + MOVD $393, R12 + B callbackasm1(SB) + MOVD $394, R12 + B callbackasm1(SB) + MOVD $395, R12 + B callbackasm1(SB) + MOVD $396, R12 + B callbackasm1(SB) + MOVD $397, R12 + B callbackasm1(SB) + MOVD $398, R12 + B callbackasm1(SB) + MOVD $399, R12 + B callbackasm1(SB) + MOVD $400, R12 + B callbackasm1(SB) + MOVD $401, R12 + B callbackasm1(SB) + MOVD $402, R12 + B callbackasm1(SB) + MOVD $403, R12 + B callbackasm1(SB) + MOVD $404, R12 + B callbackasm1(SB) + MOVD $405, R12 + B callbackasm1(SB) + MOVD $406, R12 + B callbackasm1(SB) + MOVD $407, R12 + B callbackasm1(SB) + MOVD $408, R12 + B callbackasm1(SB) + MOVD $409, R12 + B callbackasm1(SB) + MOVD $410, R12 + B callbackasm1(SB) + MOVD $411, R12 + B callbackasm1(SB) + MOVD $412, R12 + B callbackasm1(SB) + MOVD $413, R12 + B callbackasm1(SB) + MOVD $414, R12 + B callbackasm1(SB) + MOVD $415, R12 + B callbackasm1(SB) + MOVD $416, R12 + B callbackasm1(SB) + MOVD $417, R12 + B callbackasm1(SB) + MOVD $418, R12 + B callbackasm1(SB) + MOVD $419, R12 + B callbackasm1(SB) + MOVD $420, R12 + B callbackasm1(SB) + MOVD $421, R12 + B callbackasm1(SB) + MOVD $422, R12 + B callbackasm1(SB) + MOVD $423, R12 + B callbackasm1(SB) + MOVD $424, R12 + B callbackasm1(SB) + MOVD $425, R12 + B callbackasm1(SB) + MOVD $426, R12 + B callbackasm1(SB) + MOVD $427, R12 + B callbackasm1(SB) + MOVD $428, R12 + B callbackasm1(SB) + MOVD $429, R12 + B callbackasm1(SB) + MOVD $430, R12 + B callbackasm1(SB) + MOVD $431, R12 + B callbackasm1(SB) + MOVD $432, R12 + B callbackasm1(SB) + MOVD $433, R12 + B callbackasm1(SB) + MOVD $434, R12 + B callbackasm1(SB) + MOVD $435, R12 + B callbackasm1(SB) + MOVD $436, R12 + B callbackasm1(SB) + MOVD $437, R12 + B callbackasm1(SB) + MOVD $438, R12 + B callbackasm1(SB) + MOVD $439, R12 + B callbackasm1(SB) + MOVD $440, R12 + B callbackasm1(SB) + MOVD $441, R12 + B callbackasm1(SB) + MOVD $442, R12 + B callbackasm1(SB) + MOVD $443, R12 + B callbackasm1(SB) + MOVD $444, R12 + B callbackasm1(SB) + MOVD $445, R12 + B callbackasm1(SB) + MOVD $446, R12 + B callbackasm1(SB) + MOVD $447, R12 + B callbackasm1(SB) + MOVD $448, R12 + B callbackasm1(SB) + MOVD $449, R12 + B callbackasm1(SB) + MOVD $450, R12 + B callbackasm1(SB) + MOVD $451, R12 + B callbackasm1(SB) + MOVD $452, R12 + B callbackasm1(SB) + MOVD $453, R12 + B callbackasm1(SB) + MOVD $454, R12 + B callbackasm1(SB) + MOVD $455, R12 + B callbackasm1(SB) + MOVD $456, R12 + B callbackasm1(SB) + MOVD $457, R12 + B callbackasm1(SB) + MOVD $458, R12 + B callbackasm1(SB) + MOVD $459, R12 + B callbackasm1(SB) + MOVD $460, R12 + B callbackasm1(SB) + MOVD $461, R12 + B callbackasm1(SB) + MOVD $462, R12 + B callbackasm1(SB) + MOVD $463, R12 + B callbackasm1(SB) + MOVD $464, R12 + B callbackasm1(SB) + MOVD $465, R12 + B callbackasm1(SB) + MOVD $466, R12 + B callbackasm1(SB) + MOVD $467, R12 + B callbackasm1(SB) + MOVD $468, R12 + B callbackasm1(SB) + MOVD $469, R12 + B callbackasm1(SB) + MOVD $470, R12 + B callbackasm1(SB) + MOVD $471, R12 + B callbackasm1(SB) + MOVD $472, R12 + B callbackasm1(SB) + MOVD $473, R12 + B callbackasm1(SB) + MOVD $474, R12 + B callbackasm1(SB) + MOVD $475, R12 + B callbackasm1(SB) + MOVD $476, R12 + B callbackasm1(SB) + MOVD $477, R12 + B callbackasm1(SB) + MOVD $478, R12 + B callbackasm1(SB) + MOVD $479, R12 + B callbackasm1(SB) + MOVD $480, R12 + B callbackasm1(SB) + MOVD $481, R12 + B callbackasm1(SB) + MOVD $482, R12 + B callbackasm1(SB) + MOVD $483, R12 + B callbackasm1(SB) + MOVD $484, R12 + B callbackasm1(SB) + MOVD $485, R12 + B callbackasm1(SB) + MOVD $486, R12 + B callbackasm1(SB) + MOVD $487, R12 + B callbackasm1(SB) + MOVD $488, R12 + B callbackasm1(SB) + MOVD $489, R12 + B callbackasm1(SB) + MOVD $490, R12 + B callbackasm1(SB) + MOVD $491, R12 + B callbackasm1(SB) + MOVD $492, R12 + B callbackasm1(SB) + MOVD $493, R12 + B callbackasm1(SB) + MOVD $494, R12 + B callbackasm1(SB) + MOVD $495, R12 + B callbackasm1(SB) + MOVD $496, R12 + B callbackasm1(SB) + MOVD $497, R12 + B callbackasm1(SB) + MOVD $498, R12 + B callbackasm1(SB) + MOVD $499, R12 + B callbackasm1(SB) + MOVD $500, R12 + B callbackasm1(SB) + MOVD $501, R12 + B callbackasm1(SB) + MOVD $502, R12 + B callbackasm1(SB) + MOVD $503, R12 + B callbackasm1(SB) + MOVD $504, R12 + B callbackasm1(SB) + MOVD $505, R12 + B callbackasm1(SB) + MOVD $506, R12 + B callbackasm1(SB) + MOVD $507, R12 + B callbackasm1(SB) + MOVD $508, R12 + B callbackasm1(SB) + MOVD $509, R12 + B callbackasm1(SB) + MOVD $510, R12 + B callbackasm1(SB) + MOVD $511, R12 + B callbackasm1(SB) + MOVD $512, R12 + B callbackasm1(SB) + MOVD $513, R12 + B callbackasm1(SB) + MOVD $514, R12 + B callbackasm1(SB) + MOVD $515, R12 + B callbackasm1(SB) + MOVD $516, R12 + B callbackasm1(SB) + MOVD $517, R12 + B callbackasm1(SB) + MOVD $518, R12 + B callbackasm1(SB) + MOVD $519, R12 + B callbackasm1(SB) + MOVD $520, R12 + B callbackasm1(SB) + MOVD $521, R12 + B callbackasm1(SB) + MOVD $522, R12 + B callbackasm1(SB) + MOVD $523, R12 + B callbackasm1(SB) + MOVD $524, R12 + B callbackasm1(SB) + MOVD $525, R12 + B callbackasm1(SB) + MOVD $526, R12 + B callbackasm1(SB) + MOVD $527, R12 + B callbackasm1(SB) + MOVD $528, R12 + B callbackasm1(SB) + MOVD $529, R12 + B callbackasm1(SB) + MOVD $530, R12 + B callbackasm1(SB) + MOVD $531, R12 + B callbackasm1(SB) + MOVD $532, R12 + B callbackasm1(SB) + MOVD $533, R12 + B callbackasm1(SB) + MOVD $534, R12 + B callbackasm1(SB) + MOVD $535, R12 + B callbackasm1(SB) + MOVD $536, R12 + B callbackasm1(SB) + MOVD $537, R12 + B callbackasm1(SB) + MOVD $538, R12 + B callbackasm1(SB) + MOVD $539, R12 + B callbackasm1(SB) + MOVD $540, R12 + B callbackasm1(SB) + MOVD $541, R12 + B callbackasm1(SB) + MOVD $542, R12 + B callbackasm1(SB) + MOVD $543, R12 + B callbackasm1(SB) + MOVD $544, R12 + B callbackasm1(SB) + MOVD $545, R12 + B callbackasm1(SB) + MOVD $546, R12 + B callbackasm1(SB) + MOVD $547, R12 + B callbackasm1(SB) + MOVD $548, R12 + B callbackasm1(SB) + MOVD $549, R12 + B callbackasm1(SB) + MOVD $550, R12 + B callbackasm1(SB) + MOVD $551, R12 + B callbackasm1(SB) + MOVD $552, R12 + B callbackasm1(SB) + MOVD $553, R12 + B callbackasm1(SB) + MOVD $554, R12 + B callbackasm1(SB) + MOVD $555, R12 + B callbackasm1(SB) + MOVD $556, R12 + B callbackasm1(SB) + MOVD $557, R12 + B callbackasm1(SB) + MOVD $558, R12 + B callbackasm1(SB) + MOVD $559, R12 + B callbackasm1(SB) + MOVD $560, R12 + B callbackasm1(SB) + MOVD $561, R12 + B callbackasm1(SB) + MOVD $562, R12 + B callbackasm1(SB) + MOVD $563, R12 + B callbackasm1(SB) + MOVD $564, R12 + B callbackasm1(SB) + MOVD $565, R12 + B callbackasm1(SB) + MOVD $566, R12 + B callbackasm1(SB) + MOVD $567, R12 + B callbackasm1(SB) + MOVD $568, R12 + B callbackasm1(SB) + MOVD $569, R12 + B callbackasm1(SB) + MOVD $570, R12 + B callbackasm1(SB) + MOVD $571, R12 + B callbackasm1(SB) + MOVD $572, R12 + B callbackasm1(SB) + MOVD $573, R12 + B callbackasm1(SB) + MOVD $574, R12 + B callbackasm1(SB) + MOVD $575, R12 + B callbackasm1(SB) + MOVD $576, R12 + B callbackasm1(SB) + MOVD $577, R12 + B callbackasm1(SB) + MOVD $578, R12 + B callbackasm1(SB) + MOVD $579, R12 + B callbackasm1(SB) + MOVD $580, R12 + B callbackasm1(SB) + MOVD $581, R12 + B callbackasm1(SB) + MOVD $582, R12 + B callbackasm1(SB) + MOVD $583, R12 + B callbackasm1(SB) + MOVD $584, R12 + B callbackasm1(SB) + MOVD $585, R12 + B callbackasm1(SB) + MOVD $586, R12 + B callbackasm1(SB) + MOVD $587, R12 + B callbackasm1(SB) + MOVD $588, R12 + B callbackasm1(SB) + MOVD $589, R12 + B callbackasm1(SB) + MOVD $590, R12 + B callbackasm1(SB) + MOVD $591, R12 + B callbackasm1(SB) + MOVD $592, R12 + B callbackasm1(SB) + MOVD $593, R12 + B callbackasm1(SB) + MOVD $594, R12 + B callbackasm1(SB) + MOVD $595, R12 + B callbackasm1(SB) + MOVD $596, R12 + B callbackasm1(SB) + MOVD $597, R12 + B callbackasm1(SB) + MOVD $598, R12 + B callbackasm1(SB) + MOVD $599, R12 + B callbackasm1(SB) + MOVD $600, R12 + B callbackasm1(SB) + MOVD $601, R12 + B callbackasm1(SB) + MOVD $602, R12 + B callbackasm1(SB) + MOVD $603, R12 + B callbackasm1(SB) + MOVD $604, R12 + B callbackasm1(SB) + MOVD $605, R12 + B callbackasm1(SB) + MOVD $606, R12 + B callbackasm1(SB) + MOVD $607, R12 + B callbackasm1(SB) + MOVD $608, R12 + B callbackasm1(SB) + MOVD $609, R12 + B callbackasm1(SB) + MOVD $610, R12 + B callbackasm1(SB) + MOVD $611, R12 + B callbackasm1(SB) + MOVD $612, R12 + B callbackasm1(SB) + MOVD $613, R12 + B callbackasm1(SB) + MOVD $614, R12 + B callbackasm1(SB) + MOVD $615, R12 + B callbackasm1(SB) + MOVD $616, R12 + B callbackasm1(SB) + MOVD $617, R12 + B callbackasm1(SB) + MOVD $618, R12 + B callbackasm1(SB) + MOVD $619, R12 + B callbackasm1(SB) + MOVD $620, R12 + B callbackasm1(SB) + MOVD $621, R12 + B callbackasm1(SB) + MOVD $622, R12 + B callbackasm1(SB) + MOVD $623, R12 + B callbackasm1(SB) + MOVD $624, R12 + B callbackasm1(SB) + MOVD $625, R12 + B callbackasm1(SB) + MOVD $626, R12 + B callbackasm1(SB) + MOVD $627, R12 + B callbackasm1(SB) + MOVD $628, R12 + B callbackasm1(SB) + MOVD $629, R12 + B callbackasm1(SB) + MOVD $630, R12 + B callbackasm1(SB) + MOVD $631, R12 + B callbackasm1(SB) + MOVD $632, R12 + B callbackasm1(SB) + MOVD $633, R12 + B callbackasm1(SB) + MOVD $634, R12 + B callbackasm1(SB) + MOVD $635, R12 + B callbackasm1(SB) + MOVD $636, R12 + B callbackasm1(SB) + MOVD $637, R12 + B callbackasm1(SB) + MOVD $638, R12 + B callbackasm1(SB) + MOVD $639, R12 + B callbackasm1(SB) + MOVD $640, R12 + B callbackasm1(SB) + MOVD $641, R12 + B callbackasm1(SB) + MOVD $642, R12 + B callbackasm1(SB) + MOVD $643, R12 + B callbackasm1(SB) + MOVD $644, R12 + B callbackasm1(SB) + MOVD $645, R12 + B callbackasm1(SB) + MOVD $646, R12 + B callbackasm1(SB) + MOVD $647, R12 + B callbackasm1(SB) + MOVD $648, R12 + B callbackasm1(SB) + MOVD $649, R12 + B callbackasm1(SB) + MOVD $650, R12 + B callbackasm1(SB) + MOVD $651, R12 + B callbackasm1(SB) + MOVD $652, R12 + B callbackasm1(SB) + MOVD $653, R12 + B callbackasm1(SB) + MOVD $654, R12 + B callbackasm1(SB) + MOVD $655, R12 + B callbackasm1(SB) + MOVD $656, R12 + B callbackasm1(SB) + MOVD $657, R12 + B callbackasm1(SB) + MOVD $658, R12 + B callbackasm1(SB) + MOVD $659, R12 + B callbackasm1(SB) + MOVD $660, R12 + B callbackasm1(SB) + MOVD $661, R12 + B callbackasm1(SB) + MOVD $662, R12 + B callbackasm1(SB) + MOVD $663, R12 + B callbackasm1(SB) + MOVD $664, R12 + B callbackasm1(SB) + MOVD $665, R12 + B callbackasm1(SB) + MOVD $666, R12 + B callbackasm1(SB) + MOVD $667, R12 + B callbackasm1(SB) + MOVD $668, R12 + B callbackasm1(SB) + MOVD $669, R12 + B callbackasm1(SB) + MOVD $670, R12 + B callbackasm1(SB) + MOVD $671, R12 + B callbackasm1(SB) + MOVD $672, R12 + B callbackasm1(SB) + MOVD $673, R12 + B callbackasm1(SB) + MOVD $674, R12 + B callbackasm1(SB) + MOVD $675, R12 + B callbackasm1(SB) + MOVD $676, R12 + B callbackasm1(SB) + MOVD $677, R12 + B callbackasm1(SB) + MOVD $678, R12 + B callbackasm1(SB) + MOVD $679, R12 + B callbackasm1(SB) + MOVD $680, R12 + B callbackasm1(SB) + MOVD $681, R12 + B callbackasm1(SB) + MOVD $682, R12 + B callbackasm1(SB) + MOVD $683, R12 + B callbackasm1(SB) + MOVD $684, R12 + B callbackasm1(SB) + MOVD $685, R12 + B callbackasm1(SB) + MOVD $686, R12 + B callbackasm1(SB) + MOVD $687, R12 + B callbackasm1(SB) + MOVD $688, R12 + B callbackasm1(SB) + MOVD $689, R12 + B callbackasm1(SB) + MOVD $690, R12 + B callbackasm1(SB) + MOVD $691, R12 + B callbackasm1(SB) + MOVD $692, R12 + B callbackasm1(SB) + MOVD $693, R12 + B callbackasm1(SB) + MOVD $694, R12 + B callbackasm1(SB) + MOVD $695, R12 + B callbackasm1(SB) + MOVD $696, R12 + B callbackasm1(SB) + MOVD $697, R12 + B callbackasm1(SB) + MOVD $698, R12 + B callbackasm1(SB) + MOVD $699, R12 + B callbackasm1(SB) + MOVD $700, R12 + B callbackasm1(SB) + MOVD $701, R12 + B callbackasm1(SB) + MOVD $702, R12 + B callbackasm1(SB) + MOVD $703, R12 + B callbackasm1(SB) + MOVD $704, R12 + B callbackasm1(SB) + MOVD $705, R12 + B callbackasm1(SB) + MOVD $706, R12 + B callbackasm1(SB) + MOVD $707, R12 + B callbackasm1(SB) + MOVD $708, R12 + B callbackasm1(SB) + MOVD $709, R12 + B callbackasm1(SB) + MOVD $710, R12 + B callbackasm1(SB) + MOVD $711, R12 + B callbackasm1(SB) + MOVD $712, R12 + B callbackasm1(SB) + MOVD $713, R12 + B callbackasm1(SB) + MOVD $714, R12 + B callbackasm1(SB) + MOVD $715, R12 + B callbackasm1(SB) + MOVD $716, R12 + B callbackasm1(SB) + MOVD $717, R12 + B callbackasm1(SB) + MOVD $718, R12 + B callbackasm1(SB) + MOVD $719, R12 + B callbackasm1(SB) + MOVD $720, R12 + B callbackasm1(SB) + MOVD $721, R12 + B callbackasm1(SB) + MOVD $722, R12 + B callbackasm1(SB) + MOVD $723, R12 + B callbackasm1(SB) + MOVD $724, R12 + B callbackasm1(SB) + MOVD $725, R12 + B callbackasm1(SB) + MOVD $726, R12 + B callbackasm1(SB) + MOVD $727, R12 + B callbackasm1(SB) + MOVD $728, R12 + B callbackasm1(SB) + MOVD $729, R12 + B callbackasm1(SB) + MOVD $730, R12 + B callbackasm1(SB) + MOVD $731, R12 + B callbackasm1(SB) + MOVD $732, R12 + B callbackasm1(SB) + MOVD $733, R12 + B callbackasm1(SB) + MOVD $734, R12 + B callbackasm1(SB) + MOVD $735, R12 + B callbackasm1(SB) + MOVD $736, R12 + B callbackasm1(SB) + MOVD $737, R12 + B callbackasm1(SB) + MOVD $738, R12 + B callbackasm1(SB) + MOVD $739, R12 + B callbackasm1(SB) + MOVD $740, R12 + B callbackasm1(SB) + MOVD $741, R12 + B callbackasm1(SB) + MOVD $742, R12 + B callbackasm1(SB) + MOVD $743, R12 + B callbackasm1(SB) + MOVD $744, R12 + B callbackasm1(SB) + MOVD $745, R12 + B callbackasm1(SB) + MOVD $746, R12 + B callbackasm1(SB) + MOVD $747, R12 + B callbackasm1(SB) + MOVD $748, R12 + B callbackasm1(SB) + MOVD $749, R12 + B callbackasm1(SB) + MOVD $750, R12 + B callbackasm1(SB) + MOVD $751, R12 + B callbackasm1(SB) + MOVD $752, R12 + B callbackasm1(SB) + MOVD $753, R12 + B callbackasm1(SB) + MOVD $754, R12 + B callbackasm1(SB) + MOVD $755, R12 + B callbackasm1(SB) + MOVD $756, R12 + B callbackasm1(SB) + MOVD $757, R12 + B callbackasm1(SB) + MOVD $758, R12 + B callbackasm1(SB) + MOVD $759, R12 + B callbackasm1(SB) + MOVD $760, R12 + B callbackasm1(SB) + MOVD $761, R12 + B callbackasm1(SB) + MOVD $762, R12 + B callbackasm1(SB) + MOVD $763, R12 + B callbackasm1(SB) + MOVD $764, R12 + B callbackasm1(SB) + MOVD $765, R12 + B callbackasm1(SB) + MOVD $766, R12 + B callbackasm1(SB) + MOVD $767, R12 + B callbackasm1(SB) + MOVD $768, R12 + B callbackasm1(SB) + MOVD $769, R12 + B callbackasm1(SB) + MOVD $770, R12 + B callbackasm1(SB) + MOVD $771, R12 + B callbackasm1(SB) + MOVD $772, R12 + B callbackasm1(SB) + MOVD $773, R12 + B callbackasm1(SB) + MOVD $774, R12 + B callbackasm1(SB) + MOVD $775, R12 + B callbackasm1(SB) + MOVD $776, R12 + B callbackasm1(SB) + MOVD $777, R12 + B callbackasm1(SB) + MOVD $778, R12 + B callbackasm1(SB) + MOVD $779, R12 + B callbackasm1(SB) + MOVD $780, R12 + B callbackasm1(SB) + MOVD $781, R12 + B callbackasm1(SB) + MOVD $782, R12 + B callbackasm1(SB) + MOVD $783, R12 + B callbackasm1(SB) + MOVD $784, R12 + B callbackasm1(SB) + MOVD $785, R12 + B callbackasm1(SB) + MOVD $786, R12 + B callbackasm1(SB) + MOVD $787, R12 + B callbackasm1(SB) + MOVD $788, R12 + B callbackasm1(SB) + MOVD $789, R12 + B callbackasm1(SB) + MOVD $790, R12 + B callbackasm1(SB) + MOVD $791, R12 + B callbackasm1(SB) + MOVD $792, R12 + B callbackasm1(SB) + MOVD $793, R12 + B callbackasm1(SB) + MOVD $794, R12 + B callbackasm1(SB) + MOVD $795, R12 + B callbackasm1(SB) + MOVD $796, R12 + B callbackasm1(SB) + MOVD $797, R12 + B callbackasm1(SB) + MOVD $798, R12 + B callbackasm1(SB) + MOVD $799, R12 + B callbackasm1(SB) + MOVD $800, R12 + B callbackasm1(SB) + MOVD $801, R12 + B callbackasm1(SB) + MOVD $802, R12 + B callbackasm1(SB) + MOVD $803, R12 + B callbackasm1(SB) + MOVD $804, R12 + B callbackasm1(SB) + MOVD $805, R12 + B callbackasm1(SB) + MOVD $806, R12 + B callbackasm1(SB) + MOVD $807, R12 + B callbackasm1(SB) + MOVD $808, R12 + B callbackasm1(SB) + MOVD $809, R12 + B callbackasm1(SB) + MOVD $810, R12 + B callbackasm1(SB) + MOVD $811, R12 + B callbackasm1(SB) + MOVD $812, R12 + B callbackasm1(SB) + MOVD $813, R12 + B callbackasm1(SB) + MOVD $814, R12 + B callbackasm1(SB) + MOVD $815, R12 + B callbackasm1(SB) + MOVD $816, R12 + B callbackasm1(SB) + MOVD $817, R12 + B callbackasm1(SB) + MOVD $818, R12 + B callbackasm1(SB) + MOVD $819, R12 + B callbackasm1(SB) + MOVD $820, R12 + B callbackasm1(SB) + MOVD $821, R12 + B callbackasm1(SB) + MOVD $822, R12 + B callbackasm1(SB) + MOVD $823, R12 + B callbackasm1(SB) + MOVD $824, R12 + B callbackasm1(SB) + MOVD $825, R12 + B callbackasm1(SB) + MOVD $826, R12 + B callbackasm1(SB) + MOVD $827, R12 + B callbackasm1(SB) + MOVD $828, R12 + B callbackasm1(SB) + MOVD $829, R12 + B callbackasm1(SB) + MOVD $830, R12 + B callbackasm1(SB) + MOVD $831, R12 + B callbackasm1(SB) + MOVD $832, R12 + B callbackasm1(SB) + MOVD $833, R12 + B callbackasm1(SB) + MOVD $834, R12 + B callbackasm1(SB) + MOVD $835, R12 + B callbackasm1(SB) + MOVD $836, R12 + B callbackasm1(SB) + MOVD $837, R12 + B callbackasm1(SB) + MOVD $838, R12 + B callbackasm1(SB) + MOVD $839, R12 + B callbackasm1(SB) + MOVD $840, R12 + B callbackasm1(SB) + MOVD $841, R12 + B callbackasm1(SB) + MOVD $842, R12 + B callbackasm1(SB) + MOVD $843, R12 + B callbackasm1(SB) + MOVD $844, R12 + B callbackasm1(SB) + MOVD $845, R12 + B callbackasm1(SB) + MOVD $846, R12 + B callbackasm1(SB) + MOVD $847, R12 + B callbackasm1(SB) + MOVD $848, R12 + B callbackasm1(SB) + MOVD $849, R12 + B callbackasm1(SB) + MOVD $850, R12 + B callbackasm1(SB) + MOVD $851, R12 + B callbackasm1(SB) + MOVD $852, R12 + B callbackasm1(SB) + MOVD $853, R12 + B callbackasm1(SB) + MOVD $854, R12 + B callbackasm1(SB) + MOVD $855, R12 + B callbackasm1(SB) + MOVD $856, R12 + B callbackasm1(SB) + MOVD $857, R12 + B callbackasm1(SB) + MOVD $858, R12 + B callbackasm1(SB) + MOVD $859, R12 + B callbackasm1(SB) + MOVD $860, R12 + B callbackasm1(SB) + MOVD $861, R12 + B callbackasm1(SB) + MOVD $862, R12 + B callbackasm1(SB) + MOVD $863, R12 + B callbackasm1(SB) + MOVD $864, R12 + B callbackasm1(SB) + MOVD $865, R12 + B callbackasm1(SB) + MOVD $866, R12 + B callbackasm1(SB) + MOVD $867, R12 + B callbackasm1(SB) + MOVD $868, R12 + B callbackasm1(SB) + MOVD $869, R12 + B callbackasm1(SB) + MOVD $870, R12 + B callbackasm1(SB) + MOVD $871, R12 + B callbackasm1(SB) + MOVD $872, R12 + B callbackasm1(SB) + MOVD $873, R12 + B callbackasm1(SB) + MOVD $874, R12 + B callbackasm1(SB) + MOVD $875, R12 + B callbackasm1(SB) + MOVD $876, R12 + B callbackasm1(SB) + MOVD $877, R12 + B callbackasm1(SB) + MOVD $878, R12 + B callbackasm1(SB) + MOVD $879, R12 + B callbackasm1(SB) + MOVD $880, R12 + B callbackasm1(SB) + MOVD $881, R12 + B callbackasm1(SB) + MOVD $882, R12 + B callbackasm1(SB) + MOVD $883, R12 + B callbackasm1(SB) + MOVD $884, R12 + B callbackasm1(SB) + MOVD $885, R12 + B callbackasm1(SB) + MOVD $886, R12 + B callbackasm1(SB) + MOVD $887, R12 + B callbackasm1(SB) + MOVD $888, R12 + B callbackasm1(SB) + MOVD $889, R12 + B callbackasm1(SB) + MOVD $890, R12 + B callbackasm1(SB) + MOVD $891, R12 + B callbackasm1(SB) + MOVD $892, R12 + B callbackasm1(SB) + MOVD $893, R12 + B callbackasm1(SB) + MOVD $894, R12 + B callbackasm1(SB) + MOVD $895, R12 + B callbackasm1(SB) + MOVD $896, R12 + B callbackasm1(SB) + MOVD $897, R12 + B callbackasm1(SB) + MOVD $898, R12 + B callbackasm1(SB) + MOVD $899, R12 + B callbackasm1(SB) + MOVD $900, R12 + B callbackasm1(SB) + MOVD $901, R12 + B callbackasm1(SB) + MOVD $902, R12 + B callbackasm1(SB) + MOVD $903, R12 + B callbackasm1(SB) + MOVD $904, R12 + B callbackasm1(SB) + MOVD $905, R12 + B callbackasm1(SB) + MOVD $906, R12 + B callbackasm1(SB) + MOVD $907, R12 + B callbackasm1(SB) + MOVD $908, R12 + B callbackasm1(SB) + MOVD $909, R12 + B callbackasm1(SB) + MOVD $910, R12 + B callbackasm1(SB) + MOVD $911, R12 + B callbackasm1(SB) + MOVD $912, R12 + B callbackasm1(SB) + MOVD $913, R12 + B callbackasm1(SB) + MOVD $914, R12 + B callbackasm1(SB) + MOVD $915, R12 + B callbackasm1(SB) + MOVD $916, R12 + B callbackasm1(SB) + MOVD $917, R12 + B callbackasm1(SB) + MOVD $918, R12 + B callbackasm1(SB) + MOVD $919, R12 + B callbackasm1(SB) + MOVD $920, R12 + B callbackasm1(SB) + MOVD $921, R12 + B callbackasm1(SB) + MOVD $922, R12 + B callbackasm1(SB) + MOVD $923, R12 + B callbackasm1(SB) + MOVD $924, R12 + B callbackasm1(SB) + MOVD $925, R12 + B callbackasm1(SB) + MOVD $926, R12 + B callbackasm1(SB) + MOVD $927, R12 + B callbackasm1(SB) + MOVD $928, R12 + B callbackasm1(SB) + MOVD $929, R12 + B callbackasm1(SB) + MOVD $930, R12 + B callbackasm1(SB) + MOVD $931, R12 + B callbackasm1(SB) + MOVD $932, R12 + B callbackasm1(SB) + MOVD $933, R12 + B callbackasm1(SB) + MOVD $934, R12 + B callbackasm1(SB) + MOVD $935, R12 + B callbackasm1(SB) + MOVD $936, R12 + B callbackasm1(SB) + MOVD $937, R12 + B callbackasm1(SB) + MOVD $938, R12 + B callbackasm1(SB) + MOVD $939, R12 + B callbackasm1(SB) + MOVD $940, R12 + B callbackasm1(SB) + MOVD $941, R12 + B callbackasm1(SB) + MOVD $942, R12 + B callbackasm1(SB) + MOVD $943, R12 + B callbackasm1(SB) + MOVD $944, R12 + B callbackasm1(SB) + MOVD $945, R12 + B callbackasm1(SB) + MOVD $946, R12 + B callbackasm1(SB) + MOVD $947, R12 + B callbackasm1(SB) + MOVD $948, R12 + B callbackasm1(SB) + MOVD $949, R12 + B callbackasm1(SB) + MOVD $950, R12 + B callbackasm1(SB) + MOVD $951, R12 + B callbackasm1(SB) + MOVD $952, R12 + B callbackasm1(SB) + MOVD $953, R12 + B callbackasm1(SB) + MOVD $954, R12 + B callbackasm1(SB) + MOVD $955, R12 + B callbackasm1(SB) + MOVD $956, R12 + B callbackasm1(SB) + MOVD $957, R12 + B callbackasm1(SB) + MOVD $958, R12 + B callbackasm1(SB) + MOVD $959, R12 + B callbackasm1(SB) + MOVD $960, R12 + B callbackasm1(SB) + MOVD $961, R12 + B callbackasm1(SB) + MOVD $962, R12 + B callbackasm1(SB) + MOVD $963, R12 + B callbackasm1(SB) + MOVD $964, R12 + B callbackasm1(SB) + MOVD $965, R12 + B callbackasm1(SB) + MOVD $966, R12 + B callbackasm1(SB) + MOVD $967, R12 + B callbackasm1(SB) + MOVD $968, R12 + B callbackasm1(SB) + MOVD $969, R12 + B callbackasm1(SB) + MOVD $970, R12 + B callbackasm1(SB) + MOVD $971, R12 + B callbackasm1(SB) + MOVD $972, R12 + B callbackasm1(SB) + MOVD $973, R12 + B callbackasm1(SB) + MOVD $974, R12 + B callbackasm1(SB) + MOVD $975, R12 + B callbackasm1(SB) + MOVD $976, R12 + B callbackasm1(SB) + MOVD $977, R12 + B callbackasm1(SB) + MOVD $978, R12 + B callbackasm1(SB) + MOVD $979, R12 + B callbackasm1(SB) + MOVD $980, R12 + B callbackasm1(SB) + MOVD $981, R12 + B callbackasm1(SB) + MOVD $982, R12 + B callbackasm1(SB) + MOVD $983, R12 + B callbackasm1(SB) + MOVD $984, R12 + B callbackasm1(SB) + MOVD $985, R12 + B callbackasm1(SB) + MOVD $986, R12 + B callbackasm1(SB) + MOVD $987, R12 + B callbackasm1(SB) + MOVD $988, R12 + B callbackasm1(SB) + MOVD $989, R12 + B callbackasm1(SB) + MOVD $990, R12 + B callbackasm1(SB) + MOVD $991, R12 + B callbackasm1(SB) + MOVD $992, R12 + B callbackasm1(SB) + MOVD $993, R12 + B callbackasm1(SB) + MOVD $994, R12 + B callbackasm1(SB) + MOVD $995, R12 + B callbackasm1(SB) + MOVD $996, R12 + B callbackasm1(SB) + MOVD $997, R12 + B callbackasm1(SB) + MOVD $998, R12 + B callbackasm1(SB) + MOVD $999, R12 + B callbackasm1(SB) + MOVD $1000, R12 + B callbackasm1(SB) + MOVD $1001, R12 + B callbackasm1(SB) + MOVD $1002, R12 + B callbackasm1(SB) + MOVD $1003, R12 + B callbackasm1(SB) + MOVD $1004, R12 + B callbackasm1(SB) + MOVD $1005, R12 + B callbackasm1(SB) + MOVD $1006, R12 + B callbackasm1(SB) + MOVD $1007, R12 + B callbackasm1(SB) + MOVD $1008, R12 + B callbackasm1(SB) + MOVD $1009, R12 + B callbackasm1(SB) + MOVD $1010, R12 + B callbackasm1(SB) + MOVD $1011, R12 + B callbackasm1(SB) + MOVD $1012, R12 + B callbackasm1(SB) + MOVD $1013, R12 + B callbackasm1(SB) + MOVD $1014, R12 + B callbackasm1(SB) + MOVD $1015, R12 + B callbackasm1(SB) + MOVD $1016, R12 + B callbackasm1(SB) + MOVD $1017, R12 + B callbackasm1(SB) + MOVD $1018, R12 + B callbackasm1(SB) + MOVD $1019, R12 + B callbackasm1(SB) + MOVD $1020, R12 + B callbackasm1(SB) + MOVD $1021, R12 + B callbackasm1(SB) + MOVD $1022, R12 + B callbackasm1(SB) + MOVD $1023, R12 + B callbackasm1(SB) + MOVD $1024, R12 + B callbackasm1(SB) + MOVD $1025, R12 + B callbackasm1(SB) + MOVD $1026, R12 + B callbackasm1(SB) + MOVD $1027, R12 + B callbackasm1(SB) + MOVD $1028, R12 + B callbackasm1(SB) + MOVD $1029, R12 + B callbackasm1(SB) + MOVD $1030, R12 + B callbackasm1(SB) + MOVD $1031, R12 + B callbackasm1(SB) + MOVD $1032, R12 + B callbackasm1(SB) + MOVD $1033, R12 + B callbackasm1(SB) + MOVD $1034, R12 + B callbackasm1(SB) + MOVD $1035, R12 + B callbackasm1(SB) + MOVD $1036, R12 + B callbackasm1(SB) + MOVD $1037, R12 + B callbackasm1(SB) + MOVD $1038, R12 + B callbackasm1(SB) + MOVD $1039, R12 + B callbackasm1(SB) + MOVD $1040, R12 + B callbackasm1(SB) + MOVD $1041, R12 + B callbackasm1(SB) + MOVD $1042, R12 + B callbackasm1(SB) + MOVD $1043, R12 + B callbackasm1(SB) + MOVD $1044, R12 + B callbackasm1(SB) + MOVD $1045, R12 + B callbackasm1(SB) + MOVD $1046, R12 + B callbackasm1(SB) + MOVD $1047, R12 + B callbackasm1(SB) + MOVD $1048, R12 + B callbackasm1(SB) + MOVD $1049, R12 + B callbackasm1(SB) + MOVD $1050, R12 + B callbackasm1(SB) + MOVD $1051, R12 + B callbackasm1(SB) + MOVD $1052, R12 + B callbackasm1(SB) + MOVD $1053, R12 + B callbackasm1(SB) + MOVD $1054, R12 + B callbackasm1(SB) + MOVD $1055, R12 + B callbackasm1(SB) + MOVD $1056, R12 + B callbackasm1(SB) + MOVD $1057, R12 + B callbackasm1(SB) + MOVD $1058, R12 + B callbackasm1(SB) + MOVD $1059, R12 + B callbackasm1(SB) + MOVD $1060, R12 + B callbackasm1(SB) + MOVD $1061, R12 + B callbackasm1(SB) + MOVD $1062, R12 + B callbackasm1(SB) + MOVD $1063, R12 + B callbackasm1(SB) + MOVD $1064, R12 + B callbackasm1(SB) + MOVD $1065, R12 + B callbackasm1(SB) + MOVD $1066, R12 + B callbackasm1(SB) + MOVD $1067, R12 + B callbackasm1(SB) + MOVD $1068, R12 + B callbackasm1(SB) + MOVD $1069, R12 + B callbackasm1(SB) + MOVD $1070, R12 + B callbackasm1(SB) + MOVD $1071, R12 + B callbackasm1(SB) + MOVD $1072, R12 + B callbackasm1(SB) + MOVD $1073, R12 + B callbackasm1(SB) + MOVD $1074, R12 + B callbackasm1(SB) + MOVD $1075, R12 + B callbackasm1(SB) + MOVD $1076, R12 + B callbackasm1(SB) + MOVD $1077, R12 + B callbackasm1(SB) + MOVD $1078, R12 + B callbackasm1(SB) + MOVD $1079, R12 + B callbackasm1(SB) + MOVD $1080, R12 + B callbackasm1(SB) + MOVD $1081, R12 + B callbackasm1(SB) + MOVD $1082, R12 + B callbackasm1(SB) + MOVD $1083, R12 + B callbackasm1(SB) + MOVD $1084, R12 + B callbackasm1(SB) + MOVD $1085, R12 + B callbackasm1(SB) + MOVD $1086, R12 + B callbackasm1(SB) + MOVD $1087, R12 + B callbackasm1(SB) + MOVD $1088, R12 + B callbackasm1(SB) + MOVD $1089, R12 + B callbackasm1(SB) + MOVD $1090, R12 + B callbackasm1(SB) + MOVD $1091, R12 + B callbackasm1(SB) + MOVD $1092, R12 + B callbackasm1(SB) + MOVD $1093, R12 + B callbackasm1(SB) + MOVD $1094, R12 + B callbackasm1(SB) + MOVD $1095, R12 + B callbackasm1(SB) + MOVD $1096, R12 + B callbackasm1(SB) + MOVD $1097, R12 + B callbackasm1(SB) + MOVD $1098, R12 + B callbackasm1(SB) + MOVD $1099, R12 + B callbackasm1(SB) + MOVD $1100, R12 + B callbackasm1(SB) + MOVD $1101, R12 + B callbackasm1(SB) + MOVD $1102, R12 + B callbackasm1(SB) + MOVD $1103, R12 + B callbackasm1(SB) + MOVD $1104, R12 + B callbackasm1(SB) + MOVD $1105, R12 + B callbackasm1(SB) + MOVD $1106, R12 + B callbackasm1(SB) + MOVD $1107, R12 + B callbackasm1(SB) + MOVD $1108, R12 + B callbackasm1(SB) + MOVD $1109, R12 + B callbackasm1(SB) + MOVD $1110, R12 + B callbackasm1(SB) + MOVD $1111, R12 + B callbackasm1(SB) + MOVD $1112, R12 + B callbackasm1(SB) + MOVD $1113, R12 + B callbackasm1(SB) + MOVD $1114, R12 + B callbackasm1(SB) + MOVD $1115, R12 + B callbackasm1(SB) + MOVD $1116, R12 + B callbackasm1(SB) + MOVD $1117, R12 + B callbackasm1(SB) + MOVD $1118, R12 + B callbackasm1(SB) + MOVD $1119, R12 + B callbackasm1(SB) + MOVD $1120, R12 + B callbackasm1(SB) + MOVD $1121, R12 + B callbackasm1(SB) + MOVD $1122, R12 + B callbackasm1(SB) + MOVD $1123, R12 + B callbackasm1(SB) + MOVD $1124, R12 + B callbackasm1(SB) + MOVD $1125, R12 + B callbackasm1(SB) + MOVD $1126, R12 + B callbackasm1(SB) + MOVD $1127, R12 + B callbackasm1(SB) + MOVD $1128, R12 + B callbackasm1(SB) + MOVD $1129, R12 + B callbackasm1(SB) + MOVD $1130, R12 + B callbackasm1(SB) + MOVD $1131, R12 + B callbackasm1(SB) + MOVD $1132, R12 + B callbackasm1(SB) + MOVD $1133, R12 + B callbackasm1(SB) + MOVD $1134, R12 + B callbackasm1(SB) + MOVD $1135, R12 + B callbackasm1(SB) + MOVD $1136, R12 + B callbackasm1(SB) + MOVD $1137, R12 + B callbackasm1(SB) + MOVD $1138, R12 + B callbackasm1(SB) + MOVD $1139, R12 + B callbackasm1(SB) + MOVD $1140, R12 + B callbackasm1(SB) + MOVD $1141, R12 + B callbackasm1(SB) + MOVD $1142, R12 + B callbackasm1(SB) + MOVD $1143, R12 + B callbackasm1(SB) + MOVD $1144, R12 + B callbackasm1(SB) + MOVD $1145, R12 + B callbackasm1(SB) + MOVD $1146, R12 + B callbackasm1(SB) + MOVD $1147, R12 + B callbackasm1(SB) + MOVD $1148, R12 + B callbackasm1(SB) + MOVD $1149, R12 + B callbackasm1(SB) + MOVD $1150, R12 + B callbackasm1(SB) + MOVD $1151, R12 + B callbackasm1(SB) + MOVD $1152, R12 + B callbackasm1(SB) + MOVD $1153, R12 + B callbackasm1(SB) + MOVD $1154, R12 + B callbackasm1(SB) + MOVD $1155, R12 + B callbackasm1(SB) + MOVD $1156, R12 + B callbackasm1(SB) + MOVD $1157, R12 + B callbackasm1(SB) + MOVD $1158, R12 + B callbackasm1(SB) + MOVD $1159, R12 + B callbackasm1(SB) + MOVD $1160, R12 + B callbackasm1(SB) + MOVD $1161, R12 + B callbackasm1(SB) + MOVD $1162, R12 + B callbackasm1(SB) + MOVD $1163, R12 + B callbackasm1(SB) + MOVD $1164, R12 + B callbackasm1(SB) + MOVD $1165, R12 + B callbackasm1(SB) + MOVD $1166, R12 + B callbackasm1(SB) + MOVD $1167, R12 + B callbackasm1(SB) + MOVD $1168, R12 + B callbackasm1(SB) + MOVD $1169, R12 + B callbackasm1(SB) + MOVD $1170, R12 + B callbackasm1(SB) + MOVD $1171, R12 + B callbackasm1(SB) + MOVD $1172, R12 + B callbackasm1(SB) + MOVD $1173, R12 + B callbackasm1(SB) + MOVD $1174, R12 + B callbackasm1(SB) + MOVD $1175, R12 + B callbackasm1(SB) + MOVD $1176, R12 + B callbackasm1(SB) + MOVD $1177, R12 + B callbackasm1(SB) + MOVD $1178, R12 + B callbackasm1(SB) + MOVD $1179, R12 + B callbackasm1(SB) + MOVD $1180, R12 + B callbackasm1(SB) + MOVD $1181, R12 + B callbackasm1(SB) + MOVD $1182, R12 + B callbackasm1(SB) + MOVD $1183, R12 + B callbackasm1(SB) + MOVD $1184, R12 + B callbackasm1(SB) + MOVD $1185, R12 + B callbackasm1(SB) + MOVD $1186, R12 + B callbackasm1(SB) + MOVD $1187, R12 + B callbackasm1(SB) + MOVD $1188, R12 + B callbackasm1(SB) + MOVD $1189, R12 + B callbackasm1(SB) + MOVD $1190, R12 + B callbackasm1(SB) + MOVD $1191, R12 + B callbackasm1(SB) + MOVD $1192, R12 + B callbackasm1(SB) + MOVD $1193, R12 + B callbackasm1(SB) + MOVD $1194, R12 + B callbackasm1(SB) + MOVD $1195, R12 + B callbackasm1(SB) + MOVD $1196, R12 + B callbackasm1(SB) + MOVD $1197, R12 + B callbackasm1(SB) + MOVD $1198, R12 + B callbackasm1(SB) + MOVD $1199, R12 + B callbackasm1(SB) + MOVD $1200, R12 + B callbackasm1(SB) + MOVD $1201, R12 + B callbackasm1(SB) + MOVD $1202, R12 + B callbackasm1(SB) + MOVD $1203, R12 + B callbackasm1(SB) + MOVD $1204, R12 + B callbackasm1(SB) + MOVD $1205, R12 + B callbackasm1(SB) + MOVD $1206, R12 + B callbackasm1(SB) + MOVD $1207, R12 + B callbackasm1(SB) + MOVD $1208, R12 + B callbackasm1(SB) + MOVD $1209, R12 + B callbackasm1(SB) + MOVD $1210, R12 + B callbackasm1(SB) + MOVD $1211, R12 + B callbackasm1(SB) + MOVD $1212, R12 + B callbackasm1(SB) + MOVD $1213, R12 + B callbackasm1(SB) + MOVD $1214, R12 + B callbackasm1(SB) + MOVD $1215, R12 + B callbackasm1(SB) + MOVD $1216, R12 + B callbackasm1(SB) + MOVD $1217, R12 + B callbackasm1(SB) + MOVD $1218, R12 + B callbackasm1(SB) + MOVD $1219, R12 + B callbackasm1(SB) + MOVD $1220, R12 + B callbackasm1(SB) + MOVD $1221, R12 + B callbackasm1(SB) + MOVD $1222, R12 + B callbackasm1(SB) + MOVD $1223, R12 + B callbackasm1(SB) + MOVD $1224, R12 + B callbackasm1(SB) + MOVD $1225, R12 + B callbackasm1(SB) + MOVD $1226, R12 + B callbackasm1(SB) + MOVD $1227, R12 + B callbackasm1(SB) + MOVD $1228, R12 + B callbackasm1(SB) + MOVD $1229, R12 + B callbackasm1(SB) + MOVD $1230, R12 + B callbackasm1(SB) + MOVD $1231, R12 + B callbackasm1(SB) + MOVD $1232, R12 + B callbackasm1(SB) + MOVD $1233, R12 + B callbackasm1(SB) + MOVD $1234, R12 + B callbackasm1(SB) + MOVD $1235, R12 + B callbackasm1(SB) + MOVD $1236, R12 + B callbackasm1(SB) + MOVD $1237, R12 + B callbackasm1(SB) + MOVD $1238, R12 + B callbackasm1(SB) + MOVD $1239, R12 + B callbackasm1(SB) + MOVD $1240, R12 + B callbackasm1(SB) + MOVD $1241, R12 + B callbackasm1(SB) + MOVD $1242, R12 + B callbackasm1(SB) + MOVD $1243, R12 + B callbackasm1(SB) + MOVD $1244, R12 + B callbackasm1(SB) + MOVD $1245, R12 + B callbackasm1(SB) + MOVD $1246, R12 + B callbackasm1(SB) + MOVD $1247, R12 + B callbackasm1(SB) + MOVD $1248, R12 + B callbackasm1(SB) + MOVD $1249, R12 + B callbackasm1(SB) + MOVD $1250, R12 + B callbackasm1(SB) + MOVD $1251, R12 + B callbackasm1(SB) + MOVD $1252, R12 + B callbackasm1(SB) + MOVD $1253, R12 + B callbackasm1(SB) + MOVD $1254, R12 + B callbackasm1(SB) + MOVD $1255, R12 + B callbackasm1(SB) + MOVD $1256, R12 + B callbackasm1(SB) + MOVD $1257, R12 + B callbackasm1(SB) + MOVD $1258, R12 + B callbackasm1(SB) + MOVD $1259, R12 + B callbackasm1(SB) + MOVD $1260, R12 + B callbackasm1(SB) + MOVD $1261, R12 + B callbackasm1(SB) + MOVD $1262, R12 + B callbackasm1(SB) + MOVD $1263, R12 + B callbackasm1(SB) + MOVD $1264, R12 + B callbackasm1(SB) + MOVD $1265, R12 + B callbackasm1(SB) + MOVD $1266, R12 + B callbackasm1(SB) + MOVD $1267, R12 + B callbackasm1(SB) + MOVD $1268, R12 + B callbackasm1(SB) + MOVD $1269, R12 + B callbackasm1(SB) + MOVD $1270, R12 + B callbackasm1(SB) + MOVD $1271, R12 + B callbackasm1(SB) + MOVD $1272, R12 + B callbackasm1(SB) + MOVD $1273, R12 + B callbackasm1(SB) + MOVD $1274, R12 + B callbackasm1(SB) + MOVD $1275, R12 + B callbackasm1(SB) + MOVD $1276, R12 + B callbackasm1(SB) + MOVD $1277, R12 + B callbackasm1(SB) + MOVD $1278, R12 + B callbackasm1(SB) + MOVD $1279, R12 + B callbackasm1(SB) + MOVD $1280, R12 + B callbackasm1(SB) + MOVD $1281, R12 + B callbackasm1(SB) + MOVD $1282, R12 + B callbackasm1(SB) + MOVD $1283, R12 + B callbackasm1(SB) + MOVD $1284, R12 + B callbackasm1(SB) + MOVD $1285, R12 + B callbackasm1(SB) + MOVD $1286, R12 + B callbackasm1(SB) + MOVD $1287, R12 + B callbackasm1(SB) + MOVD $1288, R12 + B callbackasm1(SB) + MOVD $1289, R12 + B callbackasm1(SB) + MOVD $1290, R12 + B callbackasm1(SB) + MOVD $1291, R12 + B callbackasm1(SB) + MOVD $1292, R12 + B callbackasm1(SB) + MOVD $1293, R12 + B callbackasm1(SB) + MOVD $1294, R12 + B callbackasm1(SB) + MOVD $1295, R12 + B callbackasm1(SB) + MOVD $1296, R12 + B callbackasm1(SB) + MOVD $1297, R12 + B callbackasm1(SB) + MOVD $1298, R12 + B callbackasm1(SB) + MOVD $1299, R12 + B callbackasm1(SB) + MOVD $1300, R12 + B callbackasm1(SB) + MOVD $1301, R12 + B callbackasm1(SB) + MOVD $1302, R12 + B callbackasm1(SB) + MOVD $1303, R12 + B callbackasm1(SB) + MOVD $1304, R12 + B callbackasm1(SB) + MOVD $1305, R12 + B callbackasm1(SB) + MOVD $1306, R12 + B callbackasm1(SB) + MOVD $1307, R12 + B callbackasm1(SB) + MOVD $1308, R12 + B callbackasm1(SB) + MOVD $1309, R12 + B callbackasm1(SB) + MOVD $1310, R12 + B callbackasm1(SB) + MOVD $1311, R12 + B callbackasm1(SB) + MOVD $1312, R12 + B callbackasm1(SB) + MOVD $1313, R12 + B callbackasm1(SB) + MOVD $1314, R12 + B callbackasm1(SB) + MOVD $1315, R12 + B callbackasm1(SB) + MOVD $1316, R12 + B callbackasm1(SB) + MOVD $1317, R12 + B callbackasm1(SB) + MOVD $1318, R12 + B callbackasm1(SB) + MOVD $1319, R12 + B callbackasm1(SB) + MOVD $1320, R12 + B callbackasm1(SB) + MOVD $1321, R12 + B callbackasm1(SB) + MOVD $1322, R12 + B callbackasm1(SB) + MOVD $1323, R12 + B callbackasm1(SB) + MOVD $1324, R12 + B callbackasm1(SB) + MOVD $1325, R12 + B callbackasm1(SB) + MOVD $1326, R12 + B callbackasm1(SB) + MOVD $1327, R12 + B callbackasm1(SB) + MOVD $1328, R12 + B callbackasm1(SB) + MOVD $1329, R12 + B callbackasm1(SB) + MOVD $1330, R12 + B callbackasm1(SB) + MOVD $1331, R12 + B callbackasm1(SB) + MOVD $1332, R12 + B callbackasm1(SB) + MOVD $1333, R12 + B callbackasm1(SB) + MOVD $1334, R12 + B callbackasm1(SB) + MOVD $1335, R12 + B callbackasm1(SB) + MOVD $1336, R12 + B callbackasm1(SB) + MOVD $1337, R12 + B callbackasm1(SB) + MOVD $1338, R12 + B callbackasm1(SB) + MOVD $1339, R12 + B callbackasm1(SB) + MOVD $1340, R12 + B callbackasm1(SB) + MOVD $1341, R12 + B callbackasm1(SB) + MOVD $1342, R12 + B callbackasm1(SB) + MOVD $1343, R12 + B callbackasm1(SB) + MOVD $1344, R12 + B callbackasm1(SB) + MOVD $1345, R12 + B callbackasm1(SB) + MOVD $1346, R12 + B callbackasm1(SB) + MOVD $1347, R12 + B callbackasm1(SB) + MOVD $1348, R12 + B callbackasm1(SB) + MOVD $1349, R12 + B callbackasm1(SB) + MOVD $1350, R12 + B callbackasm1(SB) + MOVD $1351, R12 + B callbackasm1(SB) + MOVD $1352, R12 + B callbackasm1(SB) + MOVD $1353, R12 + B callbackasm1(SB) + MOVD $1354, R12 + B callbackasm1(SB) + MOVD $1355, R12 + B callbackasm1(SB) + MOVD $1356, R12 + B callbackasm1(SB) + MOVD $1357, R12 + B callbackasm1(SB) + MOVD $1358, R12 + B callbackasm1(SB) + MOVD $1359, R12 + B callbackasm1(SB) + MOVD $1360, R12 + B callbackasm1(SB) + MOVD $1361, R12 + B callbackasm1(SB) + MOVD $1362, R12 + B callbackasm1(SB) + MOVD $1363, R12 + B callbackasm1(SB) + MOVD $1364, R12 + B callbackasm1(SB) + MOVD $1365, R12 + B callbackasm1(SB) + MOVD $1366, R12 + B callbackasm1(SB) + MOVD $1367, R12 + B callbackasm1(SB) + MOVD $1368, R12 + B callbackasm1(SB) + MOVD $1369, R12 + B callbackasm1(SB) + MOVD $1370, R12 + B callbackasm1(SB) + MOVD $1371, R12 + B callbackasm1(SB) + MOVD $1372, R12 + B callbackasm1(SB) + MOVD $1373, R12 + B callbackasm1(SB) + MOVD $1374, R12 + B callbackasm1(SB) + MOVD $1375, R12 + B callbackasm1(SB) + MOVD $1376, R12 + B callbackasm1(SB) + MOVD $1377, R12 + B callbackasm1(SB) + MOVD $1378, R12 + B callbackasm1(SB) + MOVD $1379, R12 + B callbackasm1(SB) + MOVD $1380, R12 + B callbackasm1(SB) + MOVD $1381, R12 + B callbackasm1(SB) + MOVD $1382, R12 + B callbackasm1(SB) + MOVD $1383, R12 + B callbackasm1(SB) + MOVD $1384, R12 + B callbackasm1(SB) + MOVD $1385, R12 + B callbackasm1(SB) + MOVD $1386, R12 + B callbackasm1(SB) + MOVD $1387, R12 + B callbackasm1(SB) + MOVD $1388, R12 + B callbackasm1(SB) + MOVD $1389, R12 + B callbackasm1(SB) + MOVD $1390, R12 + B callbackasm1(SB) + MOVD $1391, R12 + B callbackasm1(SB) + MOVD $1392, R12 + B callbackasm1(SB) + MOVD $1393, R12 + B callbackasm1(SB) + MOVD $1394, R12 + B callbackasm1(SB) + MOVD $1395, R12 + B callbackasm1(SB) + MOVD $1396, R12 + B callbackasm1(SB) + MOVD $1397, R12 + B callbackasm1(SB) + MOVD $1398, R12 + B callbackasm1(SB) + MOVD $1399, R12 + B callbackasm1(SB) + MOVD $1400, R12 + B callbackasm1(SB) + MOVD $1401, R12 + B callbackasm1(SB) + MOVD $1402, R12 + B callbackasm1(SB) + MOVD $1403, R12 + B callbackasm1(SB) + MOVD $1404, R12 + B callbackasm1(SB) + MOVD $1405, R12 + B callbackasm1(SB) + MOVD $1406, R12 + B callbackasm1(SB) + MOVD $1407, R12 + B callbackasm1(SB) + MOVD $1408, R12 + B callbackasm1(SB) + MOVD $1409, R12 + B callbackasm1(SB) + MOVD $1410, R12 + B callbackasm1(SB) + MOVD $1411, R12 + B callbackasm1(SB) + MOVD $1412, R12 + B callbackasm1(SB) + MOVD $1413, R12 + B callbackasm1(SB) + MOVD $1414, R12 + B callbackasm1(SB) + MOVD $1415, R12 + B callbackasm1(SB) + MOVD $1416, R12 + B callbackasm1(SB) + MOVD $1417, R12 + B callbackasm1(SB) + MOVD $1418, R12 + B callbackasm1(SB) + MOVD $1419, R12 + B callbackasm1(SB) + MOVD $1420, R12 + B callbackasm1(SB) + MOVD $1421, R12 + B callbackasm1(SB) + MOVD $1422, R12 + B callbackasm1(SB) + MOVD $1423, R12 + B callbackasm1(SB) + MOVD $1424, R12 + B callbackasm1(SB) + MOVD $1425, R12 + B callbackasm1(SB) + MOVD $1426, R12 + B callbackasm1(SB) + MOVD $1427, R12 + B callbackasm1(SB) + MOVD $1428, R12 + B callbackasm1(SB) + MOVD $1429, R12 + B callbackasm1(SB) + MOVD $1430, R12 + B callbackasm1(SB) + MOVD $1431, R12 + B callbackasm1(SB) + MOVD $1432, R12 + B callbackasm1(SB) + MOVD $1433, R12 + B callbackasm1(SB) + MOVD $1434, R12 + B callbackasm1(SB) + MOVD $1435, R12 + B callbackasm1(SB) + MOVD $1436, R12 + B callbackasm1(SB) + MOVD $1437, R12 + B callbackasm1(SB) + MOVD $1438, R12 + B callbackasm1(SB) + MOVD $1439, R12 + B callbackasm1(SB) + MOVD $1440, R12 + B callbackasm1(SB) + MOVD $1441, R12 + B callbackasm1(SB) + MOVD $1442, R12 + B callbackasm1(SB) + MOVD $1443, R12 + B callbackasm1(SB) + MOVD $1444, R12 + B callbackasm1(SB) + MOVD $1445, R12 + B callbackasm1(SB) + MOVD $1446, R12 + B callbackasm1(SB) + MOVD $1447, R12 + B callbackasm1(SB) + MOVD $1448, R12 + B callbackasm1(SB) + MOVD $1449, R12 + B callbackasm1(SB) + MOVD $1450, R12 + B callbackasm1(SB) + MOVD $1451, R12 + B callbackasm1(SB) + MOVD $1452, R12 + B callbackasm1(SB) + MOVD $1453, R12 + B callbackasm1(SB) + MOVD $1454, R12 + B callbackasm1(SB) + MOVD $1455, R12 + B callbackasm1(SB) + MOVD $1456, R12 + B callbackasm1(SB) + MOVD $1457, R12 + B callbackasm1(SB) + MOVD $1458, R12 + B callbackasm1(SB) + MOVD $1459, R12 + B callbackasm1(SB) + MOVD $1460, R12 + B callbackasm1(SB) + MOVD $1461, R12 + B callbackasm1(SB) + MOVD $1462, R12 + B callbackasm1(SB) + MOVD $1463, R12 + B callbackasm1(SB) + MOVD $1464, R12 + B callbackasm1(SB) + MOVD $1465, R12 + B callbackasm1(SB) + MOVD $1466, R12 + B callbackasm1(SB) + MOVD $1467, R12 + B callbackasm1(SB) + MOVD $1468, R12 + B callbackasm1(SB) + MOVD $1469, R12 + B callbackasm1(SB) + MOVD $1470, R12 + B callbackasm1(SB) + MOVD $1471, R12 + B callbackasm1(SB) + MOVD $1472, R12 + B callbackasm1(SB) + MOVD $1473, R12 + B callbackasm1(SB) + MOVD $1474, R12 + B callbackasm1(SB) + MOVD $1475, R12 + B callbackasm1(SB) + MOVD $1476, R12 + B callbackasm1(SB) + MOVD $1477, R12 + B callbackasm1(SB) + MOVD $1478, R12 + B callbackasm1(SB) + MOVD $1479, R12 + B callbackasm1(SB) + MOVD $1480, R12 + B callbackasm1(SB) + MOVD $1481, R12 + B callbackasm1(SB) + MOVD $1482, R12 + B callbackasm1(SB) + MOVD $1483, R12 + B callbackasm1(SB) + MOVD $1484, R12 + B callbackasm1(SB) + MOVD $1485, R12 + B callbackasm1(SB) + MOVD $1486, R12 + B callbackasm1(SB) + MOVD $1487, R12 + B callbackasm1(SB) + MOVD $1488, R12 + B callbackasm1(SB) + MOVD $1489, R12 + B callbackasm1(SB) + MOVD $1490, R12 + B callbackasm1(SB) + MOVD $1491, R12 + B callbackasm1(SB) + MOVD $1492, R12 + B callbackasm1(SB) + MOVD $1493, R12 + B callbackasm1(SB) + MOVD $1494, R12 + B callbackasm1(SB) + MOVD $1495, R12 + B callbackasm1(SB) + MOVD $1496, R12 + B callbackasm1(SB) + MOVD $1497, R12 + B callbackasm1(SB) + MOVD $1498, R12 + B callbackasm1(SB) + MOVD $1499, R12 + B callbackasm1(SB) + MOVD $1500, R12 + B callbackasm1(SB) + MOVD $1501, R12 + B callbackasm1(SB) + MOVD $1502, R12 + B callbackasm1(SB) + MOVD $1503, R12 + B callbackasm1(SB) + MOVD $1504, R12 + B callbackasm1(SB) + MOVD $1505, R12 + B callbackasm1(SB) + MOVD $1506, R12 + B callbackasm1(SB) + MOVD $1507, R12 + B callbackasm1(SB) + MOVD $1508, R12 + B callbackasm1(SB) + MOVD $1509, R12 + B callbackasm1(SB) + MOVD $1510, R12 + B callbackasm1(SB) + MOVD $1511, R12 + B callbackasm1(SB) + MOVD $1512, R12 + B callbackasm1(SB) + MOVD $1513, R12 + B callbackasm1(SB) + MOVD $1514, R12 + B callbackasm1(SB) + MOVD $1515, R12 + B callbackasm1(SB) + MOVD $1516, R12 + B callbackasm1(SB) + MOVD $1517, R12 + B callbackasm1(SB) + MOVD $1518, R12 + B callbackasm1(SB) + MOVD $1519, R12 + B callbackasm1(SB) + MOVD $1520, R12 + B callbackasm1(SB) + MOVD $1521, R12 + B callbackasm1(SB) + MOVD $1522, R12 + B callbackasm1(SB) + MOVD $1523, R12 + B callbackasm1(SB) + MOVD $1524, R12 + B callbackasm1(SB) + MOVD $1525, R12 + B callbackasm1(SB) + MOVD $1526, R12 + B callbackasm1(SB) + MOVD $1527, R12 + B callbackasm1(SB) + MOVD $1528, R12 + B callbackasm1(SB) + MOVD $1529, R12 + B callbackasm1(SB) + MOVD $1530, R12 + B callbackasm1(SB) + MOVD $1531, R12 + B callbackasm1(SB) + MOVD $1532, R12 + B callbackasm1(SB) + MOVD $1533, R12 + B callbackasm1(SB) + MOVD $1534, R12 + B callbackasm1(SB) + MOVD $1535, R12 + B callbackasm1(SB) + MOVD $1536, R12 + B callbackasm1(SB) + MOVD $1537, R12 + B callbackasm1(SB) + MOVD $1538, R12 + B callbackasm1(SB) + MOVD $1539, R12 + B callbackasm1(SB) + MOVD $1540, R12 + B callbackasm1(SB) + MOVD $1541, R12 + B callbackasm1(SB) + MOVD $1542, R12 + B callbackasm1(SB) + MOVD $1543, R12 + B callbackasm1(SB) + MOVD $1544, R12 + B callbackasm1(SB) + MOVD $1545, R12 + B callbackasm1(SB) + MOVD $1546, R12 + B callbackasm1(SB) + MOVD $1547, R12 + B callbackasm1(SB) + MOVD $1548, R12 + B callbackasm1(SB) + MOVD $1549, R12 + B callbackasm1(SB) + MOVD $1550, R12 + B callbackasm1(SB) + MOVD $1551, R12 + B callbackasm1(SB) + MOVD $1552, R12 + B callbackasm1(SB) + MOVD $1553, R12 + B callbackasm1(SB) + MOVD $1554, R12 + B callbackasm1(SB) + MOVD $1555, R12 + B callbackasm1(SB) + MOVD $1556, R12 + B callbackasm1(SB) + MOVD $1557, R12 + B callbackasm1(SB) + MOVD $1558, R12 + B callbackasm1(SB) + MOVD $1559, R12 + B callbackasm1(SB) + MOVD $1560, R12 + B callbackasm1(SB) + MOVD $1561, R12 + B callbackasm1(SB) + MOVD $1562, R12 + B callbackasm1(SB) + MOVD $1563, R12 + B callbackasm1(SB) + MOVD $1564, R12 + B callbackasm1(SB) + MOVD $1565, R12 + B callbackasm1(SB) + MOVD $1566, R12 + B callbackasm1(SB) + MOVD $1567, R12 + B callbackasm1(SB) + MOVD $1568, R12 + B callbackasm1(SB) + MOVD $1569, R12 + B callbackasm1(SB) + MOVD $1570, R12 + B callbackasm1(SB) + MOVD $1571, R12 + B callbackasm1(SB) + MOVD $1572, R12 + B callbackasm1(SB) + MOVD $1573, R12 + B callbackasm1(SB) + MOVD $1574, R12 + B callbackasm1(SB) + MOVD $1575, R12 + B callbackasm1(SB) + MOVD $1576, R12 + B callbackasm1(SB) + MOVD $1577, R12 + B callbackasm1(SB) + MOVD $1578, R12 + B callbackasm1(SB) + MOVD $1579, R12 + B callbackasm1(SB) + MOVD $1580, R12 + B callbackasm1(SB) + MOVD $1581, R12 + B callbackasm1(SB) + MOVD $1582, R12 + B callbackasm1(SB) + MOVD $1583, R12 + B callbackasm1(SB) + MOVD $1584, R12 + B callbackasm1(SB) + MOVD $1585, R12 + B callbackasm1(SB) + MOVD $1586, R12 + B callbackasm1(SB) + MOVD $1587, R12 + B callbackasm1(SB) + MOVD $1588, R12 + B callbackasm1(SB) + MOVD $1589, R12 + B callbackasm1(SB) + MOVD $1590, R12 + B callbackasm1(SB) + MOVD $1591, R12 + B callbackasm1(SB) + MOVD $1592, R12 + B callbackasm1(SB) + MOVD $1593, R12 + B callbackasm1(SB) + MOVD $1594, R12 + B callbackasm1(SB) + MOVD $1595, R12 + B callbackasm1(SB) + MOVD $1596, R12 + B callbackasm1(SB) + MOVD $1597, R12 + B callbackasm1(SB) + MOVD $1598, R12 + B callbackasm1(SB) + MOVD $1599, R12 + B callbackasm1(SB) + MOVD $1600, R12 + B callbackasm1(SB) + MOVD $1601, R12 + B callbackasm1(SB) + MOVD $1602, R12 + B callbackasm1(SB) + MOVD $1603, R12 + B callbackasm1(SB) + MOVD $1604, R12 + B callbackasm1(SB) + MOVD $1605, R12 + B callbackasm1(SB) + MOVD $1606, R12 + B callbackasm1(SB) + MOVD $1607, R12 + B callbackasm1(SB) + MOVD $1608, R12 + B callbackasm1(SB) + MOVD $1609, R12 + B callbackasm1(SB) + MOVD $1610, R12 + B callbackasm1(SB) + MOVD $1611, R12 + B callbackasm1(SB) + MOVD $1612, R12 + B callbackasm1(SB) + MOVD $1613, R12 + B callbackasm1(SB) + MOVD $1614, R12 + B callbackasm1(SB) + MOVD $1615, R12 + B callbackasm1(SB) + MOVD $1616, R12 + B callbackasm1(SB) + MOVD $1617, R12 + B callbackasm1(SB) + MOVD $1618, R12 + B callbackasm1(SB) + MOVD $1619, R12 + B callbackasm1(SB) + MOVD $1620, R12 + B callbackasm1(SB) + MOVD $1621, R12 + B callbackasm1(SB) + MOVD $1622, R12 + B callbackasm1(SB) + MOVD $1623, R12 + B callbackasm1(SB) + MOVD $1624, R12 + B callbackasm1(SB) + MOVD $1625, R12 + B callbackasm1(SB) + MOVD $1626, R12 + B callbackasm1(SB) + MOVD $1627, R12 + B callbackasm1(SB) + MOVD $1628, R12 + B callbackasm1(SB) + MOVD $1629, R12 + B callbackasm1(SB) + MOVD $1630, R12 + B callbackasm1(SB) + MOVD $1631, R12 + B callbackasm1(SB) + MOVD $1632, R12 + B callbackasm1(SB) + MOVD $1633, R12 + B callbackasm1(SB) + MOVD $1634, R12 + B callbackasm1(SB) + MOVD $1635, R12 + B callbackasm1(SB) + MOVD $1636, R12 + B callbackasm1(SB) + MOVD $1637, R12 + B callbackasm1(SB) + MOVD $1638, R12 + B callbackasm1(SB) + MOVD $1639, R12 + B callbackasm1(SB) + MOVD $1640, R12 + B callbackasm1(SB) + MOVD $1641, R12 + B callbackasm1(SB) + MOVD $1642, R12 + B callbackasm1(SB) + MOVD $1643, R12 + B callbackasm1(SB) + MOVD $1644, R12 + B callbackasm1(SB) + MOVD $1645, R12 + B callbackasm1(SB) + MOVD $1646, R12 + B callbackasm1(SB) + MOVD $1647, R12 + B callbackasm1(SB) + MOVD $1648, R12 + B callbackasm1(SB) + MOVD $1649, R12 + B callbackasm1(SB) + MOVD $1650, R12 + B callbackasm1(SB) + MOVD $1651, R12 + B callbackasm1(SB) + MOVD $1652, R12 + B callbackasm1(SB) + MOVD $1653, R12 + B callbackasm1(SB) + MOVD $1654, R12 + B callbackasm1(SB) + MOVD $1655, R12 + B callbackasm1(SB) + MOVD $1656, R12 + B callbackasm1(SB) + MOVD $1657, R12 + B callbackasm1(SB) + MOVD $1658, R12 + B callbackasm1(SB) + MOVD $1659, R12 + B callbackasm1(SB) + MOVD $1660, R12 + B callbackasm1(SB) + MOVD $1661, R12 + B callbackasm1(SB) + MOVD $1662, R12 + B callbackasm1(SB) + MOVD $1663, R12 + B callbackasm1(SB) + MOVD $1664, R12 + B callbackasm1(SB) + MOVD $1665, R12 + B callbackasm1(SB) + MOVD $1666, R12 + B callbackasm1(SB) + MOVD $1667, R12 + B callbackasm1(SB) + MOVD $1668, R12 + B callbackasm1(SB) + MOVD $1669, R12 + B callbackasm1(SB) + MOVD $1670, R12 + B callbackasm1(SB) + MOVD $1671, R12 + B callbackasm1(SB) + MOVD $1672, R12 + B callbackasm1(SB) + MOVD $1673, R12 + B callbackasm1(SB) + MOVD $1674, R12 + B callbackasm1(SB) + MOVD $1675, R12 + B callbackasm1(SB) + MOVD $1676, R12 + B callbackasm1(SB) + MOVD $1677, R12 + B callbackasm1(SB) + MOVD $1678, R12 + B callbackasm1(SB) + MOVD $1679, R12 + B callbackasm1(SB) + MOVD $1680, R12 + B callbackasm1(SB) + MOVD $1681, R12 + B callbackasm1(SB) + MOVD $1682, R12 + B callbackasm1(SB) + MOVD $1683, R12 + B callbackasm1(SB) + MOVD $1684, R12 + B callbackasm1(SB) + MOVD $1685, R12 + B callbackasm1(SB) + MOVD $1686, R12 + B callbackasm1(SB) + MOVD $1687, R12 + B callbackasm1(SB) + MOVD $1688, R12 + B callbackasm1(SB) + MOVD $1689, R12 + B callbackasm1(SB) + MOVD $1690, R12 + B callbackasm1(SB) + MOVD $1691, R12 + B callbackasm1(SB) + MOVD $1692, R12 + B callbackasm1(SB) + MOVD $1693, R12 + B callbackasm1(SB) + MOVD $1694, R12 + B callbackasm1(SB) + MOVD $1695, R12 + B callbackasm1(SB) + MOVD $1696, R12 + B callbackasm1(SB) + MOVD $1697, R12 + B callbackasm1(SB) + MOVD $1698, R12 + B callbackasm1(SB) + MOVD $1699, R12 + B callbackasm1(SB) + MOVD $1700, R12 + B callbackasm1(SB) + MOVD $1701, R12 + B callbackasm1(SB) + MOVD $1702, R12 + B callbackasm1(SB) + MOVD $1703, R12 + B callbackasm1(SB) + MOVD $1704, R12 + B callbackasm1(SB) + MOVD $1705, R12 + B callbackasm1(SB) + MOVD $1706, R12 + B callbackasm1(SB) + MOVD $1707, R12 + B callbackasm1(SB) + MOVD $1708, R12 + B callbackasm1(SB) + MOVD $1709, R12 + B callbackasm1(SB) + MOVD $1710, R12 + B callbackasm1(SB) + MOVD $1711, R12 + B callbackasm1(SB) + MOVD $1712, R12 + B callbackasm1(SB) + MOVD $1713, R12 + B callbackasm1(SB) + MOVD $1714, R12 + B callbackasm1(SB) + MOVD $1715, R12 + B callbackasm1(SB) + MOVD $1716, R12 + B callbackasm1(SB) + MOVD $1717, R12 + B callbackasm1(SB) + MOVD $1718, R12 + B callbackasm1(SB) + MOVD $1719, R12 + B callbackasm1(SB) + MOVD $1720, R12 + B callbackasm1(SB) + MOVD $1721, R12 + B callbackasm1(SB) + MOVD $1722, R12 + B callbackasm1(SB) + MOVD $1723, R12 + B callbackasm1(SB) + MOVD $1724, R12 + B callbackasm1(SB) + MOVD $1725, R12 + B callbackasm1(SB) + MOVD $1726, R12 + B callbackasm1(SB) + MOVD $1727, R12 + B callbackasm1(SB) + MOVD $1728, R12 + B callbackasm1(SB) + MOVD $1729, R12 + B callbackasm1(SB) + MOVD $1730, R12 + B callbackasm1(SB) + MOVD $1731, R12 + B callbackasm1(SB) + MOVD $1732, R12 + B callbackasm1(SB) + MOVD $1733, R12 + B callbackasm1(SB) + MOVD $1734, R12 + B callbackasm1(SB) + MOVD $1735, R12 + B callbackasm1(SB) + MOVD $1736, R12 + B callbackasm1(SB) + MOVD $1737, R12 + B callbackasm1(SB) + MOVD $1738, R12 + B callbackasm1(SB) + MOVD $1739, R12 + B callbackasm1(SB) + MOVD $1740, R12 + B callbackasm1(SB) + MOVD $1741, R12 + B callbackasm1(SB) + MOVD $1742, R12 + B callbackasm1(SB) + MOVD $1743, R12 + B callbackasm1(SB) + MOVD $1744, R12 + B callbackasm1(SB) + MOVD $1745, R12 + B callbackasm1(SB) + MOVD $1746, R12 + B callbackasm1(SB) + MOVD $1747, R12 + B callbackasm1(SB) + MOVD $1748, R12 + B callbackasm1(SB) + MOVD $1749, R12 + B callbackasm1(SB) + MOVD $1750, R12 + B callbackasm1(SB) + MOVD $1751, R12 + B callbackasm1(SB) + MOVD $1752, R12 + B callbackasm1(SB) + MOVD $1753, R12 + B callbackasm1(SB) + MOVD $1754, R12 + B callbackasm1(SB) + MOVD $1755, R12 + B callbackasm1(SB) + MOVD $1756, R12 + B callbackasm1(SB) + MOVD $1757, R12 + B callbackasm1(SB) + MOVD $1758, R12 + B callbackasm1(SB) + MOVD $1759, R12 + B callbackasm1(SB) + MOVD $1760, R12 + B callbackasm1(SB) + MOVD $1761, R12 + B callbackasm1(SB) + MOVD $1762, R12 + B callbackasm1(SB) + MOVD $1763, R12 + B callbackasm1(SB) + MOVD $1764, R12 + B callbackasm1(SB) + MOVD $1765, R12 + B callbackasm1(SB) + MOVD $1766, R12 + B callbackasm1(SB) + MOVD $1767, R12 + B callbackasm1(SB) + MOVD $1768, R12 + B callbackasm1(SB) + MOVD $1769, R12 + B callbackasm1(SB) + MOVD $1770, R12 + B callbackasm1(SB) + MOVD $1771, R12 + B callbackasm1(SB) + MOVD $1772, R12 + B callbackasm1(SB) + MOVD $1773, R12 + B callbackasm1(SB) + MOVD $1774, R12 + B callbackasm1(SB) + MOVD $1775, R12 + B callbackasm1(SB) + MOVD $1776, R12 + B callbackasm1(SB) + MOVD $1777, R12 + B callbackasm1(SB) + MOVD $1778, R12 + B callbackasm1(SB) + MOVD $1779, R12 + B callbackasm1(SB) + MOVD $1780, R12 + B callbackasm1(SB) + MOVD $1781, R12 + B callbackasm1(SB) + MOVD $1782, R12 + B callbackasm1(SB) + MOVD $1783, R12 + B callbackasm1(SB) + MOVD $1784, R12 + B callbackasm1(SB) + MOVD $1785, R12 + B callbackasm1(SB) + MOVD $1786, R12 + B callbackasm1(SB) + MOVD $1787, R12 + B callbackasm1(SB) + MOVD $1788, R12 + B callbackasm1(SB) + MOVD $1789, R12 + B callbackasm1(SB) + MOVD $1790, R12 + B callbackasm1(SB) + MOVD $1791, R12 + B callbackasm1(SB) + MOVD $1792, R12 + B callbackasm1(SB) + MOVD $1793, R12 + B callbackasm1(SB) + MOVD $1794, R12 + B callbackasm1(SB) + MOVD $1795, R12 + B callbackasm1(SB) + MOVD $1796, R12 + B callbackasm1(SB) + MOVD $1797, R12 + B callbackasm1(SB) + MOVD $1798, R12 + B callbackasm1(SB) + MOVD $1799, R12 + B callbackasm1(SB) + MOVD $1800, R12 + B callbackasm1(SB) + MOVD $1801, R12 + B callbackasm1(SB) + MOVD $1802, R12 + B callbackasm1(SB) + MOVD $1803, R12 + B callbackasm1(SB) + MOVD $1804, R12 + B callbackasm1(SB) + MOVD $1805, R12 + B callbackasm1(SB) + MOVD $1806, R12 + B callbackasm1(SB) + MOVD $1807, R12 + B callbackasm1(SB) + MOVD $1808, R12 + B callbackasm1(SB) + MOVD $1809, R12 + B callbackasm1(SB) + MOVD $1810, R12 + B callbackasm1(SB) + MOVD $1811, R12 + B callbackasm1(SB) + MOVD $1812, R12 + B callbackasm1(SB) + MOVD $1813, R12 + B callbackasm1(SB) + MOVD $1814, R12 + B callbackasm1(SB) + MOVD $1815, R12 + B callbackasm1(SB) + MOVD $1816, R12 + B callbackasm1(SB) + MOVD $1817, R12 + B callbackasm1(SB) + MOVD $1818, R12 + B callbackasm1(SB) + MOVD $1819, R12 + B callbackasm1(SB) + MOVD $1820, R12 + B callbackasm1(SB) + MOVD $1821, R12 + B callbackasm1(SB) + MOVD $1822, R12 + B callbackasm1(SB) + MOVD $1823, R12 + B callbackasm1(SB) + MOVD $1824, R12 + B callbackasm1(SB) + MOVD $1825, R12 + B callbackasm1(SB) + MOVD $1826, R12 + B callbackasm1(SB) + MOVD $1827, R12 + B callbackasm1(SB) + MOVD $1828, R12 + B callbackasm1(SB) + MOVD $1829, R12 + B callbackasm1(SB) + MOVD $1830, R12 + B callbackasm1(SB) + MOVD $1831, R12 + B callbackasm1(SB) + MOVD $1832, R12 + B callbackasm1(SB) + MOVD $1833, R12 + B callbackasm1(SB) + MOVD $1834, R12 + B callbackasm1(SB) + MOVD $1835, R12 + B callbackasm1(SB) + MOVD $1836, R12 + B callbackasm1(SB) + MOVD $1837, R12 + B callbackasm1(SB) + MOVD $1838, R12 + B callbackasm1(SB) + MOVD $1839, R12 + B callbackasm1(SB) + MOVD $1840, R12 + B callbackasm1(SB) + MOVD $1841, R12 + B callbackasm1(SB) + MOVD $1842, R12 + B callbackasm1(SB) + MOVD $1843, R12 + B callbackasm1(SB) + MOVD $1844, R12 + B callbackasm1(SB) + MOVD $1845, R12 + B callbackasm1(SB) + MOVD $1846, R12 + B callbackasm1(SB) + MOVD $1847, R12 + B callbackasm1(SB) + MOVD $1848, R12 + B callbackasm1(SB) + MOVD $1849, R12 + B callbackasm1(SB) + MOVD $1850, R12 + B callbackasm1(SB) + MOVD $1851, R12 + B callbackasm1(SB) + MOVD $1852, R12 + B callbackasm1(SB) + MOVD $1853, R12 + B callbackasm1(SB) + MOVD $1854, R12 + B callbackasm1(SB) + MOVD $1855, R12 + B callbackasm1(SB) + MOVD $1856, R12 + B callbackasm1(SB) + MOVD $1857, R12 + B callbackasm1(SB) + MOVD $1858, R12 + B callbackasm1(SB) + MOVD $1859, R12 + B callbackasm1(SB) + MOVD $1860, R12 + B callbackasm1(SB) + MOVD $1861, R12 + B callbackasm1(SB) + MOVD $1862, R12 + B callbackasm1(SB) + MOVD $1863, R12 + B callbackasm1(SB) + MOVD $1864, R12 + B callbackasm1(SB) + MOVD $1865, R12 + B callbackasm1(SB) + MOVD $1866, R12 + B callbackasm1(SB) + MOVD $1867, R12 + B callbackasm1(SB) + MOVD $1868, R12 + B callbackasm1(SB) + MOVD $1869, R12 + B callbackasm1(SB) + MOVD $1870, R12 + B callbackasm1(SB) + MOVD $1871, R12 + B callbackasm1(SB) + MOVD $1872, R12 + B callbackasm1(SB) + MOVD $1873, R12 + B callbackasm1(SB) + MOVD $1874, R12 + B callbackasm1(SB) + MOVD $1875, R12 + B callbackasm1(SB) + MOVD $1876, R12 + B callbackasm1(SB) + MOVD $1877, R12 + B callbackasm1(SB) + MOVD $1878, R12 + B callbackasm1(SB) + MOVD $1879, R12 + B callbackasm1(SB) + MOVD $1880, R12 + B callbackasm1(SB) + MOVD $1881, R12 + B callbackasm1(SB) + MOVD $1882, R12 + B callbackasm1(SB) + MOVD $1883, R12 + B callbackasm1(SB) + MOVD $1884, R12 + B callbackasm1(SB) + MOVD $1885, R12 + B callbackasm1(SB) + MOVD $1886, R12 + B callbackasm1(SB) + MOVD $1887, R12 + B callbackasm1(SB) + MOVD $1888, R12 + B callbackasm1(SB) + MOVD $1889, R12 + B callbackasm1(SB) + MOVD $1890, R12 + B callbackasm1(SB) + MOVD $1891, R12 + B callbackasm1(SB) + MOVD $1892, R12 + B callbackasm1(SB) + MOVD $1893, R12 + B callbackasm1(SB) + MOVD $1894, R12 + B callbackasm1(SB) + MOVD $1895, R12 + B callbackasm1(SB) + MOVD $1896, R12 + B callbackasm1(SB) + MOVD $1897, R12 + B callbackasm1(SB) + MOVD $1898, R12 + B callbackasm1(SB) + MOVD $1899, R12 + B callbackasm1(SB) + MOVD $1900, R12 + B callbackasm1(SB) + MOVD $1901, R12 + B callbackasm1(SB) + MOVD $1902, R12 + B callbackasm1(SB) + MOVD $1903, R12 + B callbackasm1(SB) + MOVD $1904, R12 + B callbackasm1(SB) + MOVD $1905, R12 + B callbackasm1(SB) + MOVD $1906, R12 + B callbackasm1(SB) + MOVD $1907, R12 + B callbackasm1(SB) + MOVD $1908, R12 + B callbackasm1(SB) + MOVD $1909, R12 + B callbackasm1(SB) + MOVD $1910, R12 + B callbackasm1(SB) + MOVD $1911, R12 + B callbackasm1(SB) + MOVD $1912, R12 + B callbackasm1(SB) + MOVD $1913, R12 + B callbackasm1(SB) + MOVD $1914, R12 + B callbackasm1(SB) + MOVD $1915, R12 + B callbackasm1(SB) + MOVD $1916, R12 + B callbackasm1(SB) + MOVD $1917, R12 + B callbackasm1(SB) + MOVD $1918, R12 + B callbackasm1(SB) + MOVD $1919, R12 + B callbackasm1(SB) + MOVD $1920, R12 + B callbackasm1(SB) + MOVD $1921, R12 + B callbackasm1(SB) + MOVD $1922, R12 + B callbackasm1(SB) + MOVD $1923, R12 + B callbackasm1(SB) + MOVD $1924, R12 + B callbackasm1(SB) + MOVD $1925, R12 + B callbackasm1(SB) + MOVD $1926, R12 + B callbackasm1(SB) + MOVD $1927, R12 + B callbackasm1(SB) + MOVD $1928, R12 + B callbackasm1(SB) + MOVD $1929, R12 + B callbackasm1(SB) + MOVD $1930, R12 + B callbackasm1(SB) + MOVD $1931, R12 + B callbackasm1(SB) + MOVD $1932, R12 + B callbackasm1(SB) + MOVD $1933, R12 + B callbackasm1(SB) + MOVD $1934, R12 + B callbackasm1(SB) + MOVD $1935, R12 + B callbackasm1(SB) + MOVD $1936, R12 + B callbackasm1(SB) + MOVD $1937, R12 + B callbackasm1(SB) + MOVD $1938, R12 + B callbackasm1(SB) + MOVD $1939, R12 + B callbackasm1(SB) + MOVD $1940, R12 + B callbackasm1(SB) + MOVD $1941, R12 + B callbackasm1(SB) + MOVD $1942, R12 + B callbackasm1(SB) + MOVD $1943, R12 + B callbackasm1(SB) + MOVD $1944, R12 + B callbackasm1(SB) + MOVD $1945, R12 + B callbackasm1(SB) + MOVD $1946, R12 + B callbackasm1(SB) + MOVD $1947, R12 + B callbackasm1(SB) + MOVD $1948, R12 + B callbackasm1(SB) + MOVD $1949, R12 + B callbackasm1(SB) + MOVD $1950, R12 + B callbackasm1(SB) + MOVD $1951, R12 + B callbackasm1(SB) + MOVD $1952, R12 + B callbackasm1(SB) + MOVD $1953, R12 + B callbackasm1(SB) + MOVD $1954, R12 + B callbackasm1(SB) + MOVD $1955, R12 + B callbackasm1(SB) + MOVD $1956, R12 + B callbackasm1(SB) + MOVD $1957, R12 + B callbackasm1(SB) + MOVD $1958, R12 + B callbackasm1(SB) + MOVD $1959, R12 + B callbackasm1(SB) + MOVD $1960, R12 + B callbackasm1(SB) + MOVD $1961, R12 + B callbackasm1(SB) + MOVD $1962, R12 + B callbackasm1(SB) + MOVD $1963, R12 + B callbackasm1(SB) + MOVD $1964, R12 + B callbackasm1(SB) + MOVD $1965, R12 + B callbackasm1(SB) + MOVD $1966, R12 + B callbackasm1(SB) + MOVD $1967, R12 + B callbackasm1(SB) + MOVD $1968, R12 + B callbackasm1(SB) + MOVD $1969, R12 + B callbackasm1(SB) + MOVD $1970, R12 + B callbackasm1(SB) + MOVD $1971, R12 + B callbackasm1(SB) + MOVD $1972, R12 + B callbackasm1(SB) + MOVD $1973, R12 + B callbackasm1(SB) + MOVD $1974, R12 + B callbackasm1(SB) + MOVD $1975, R12 + B callbackasm1(SB) + MOVD $1976, R12 + B callbackasm1(SB) + MOVD $1977, R12 + B callbackasm1(SB) + MOVD $1978, R12 + B callbackasm1(SB) + MOVD $1979, R12 + B callbackasm1(SB) + MOVD $1980, R12 + B callbackasm1(SB) + MOVD $1981, R12 + B callbackasm1(SB) + MOVD $1982, R12 + B callbackasm1(SB) + MOVD $1983, R12 + B callbackasm1(SB) + MOVD $1984, R12 + B callbackasm1(SB) + MOVD $1985, R12 + B callbackasm1(SB) + MOVD $1986, R12 + B callbackasm1(SB) + MOVD $1987, R12 + B callbackasm1(SB) + MOVD $1988, R12 + B callbackasm1(SB) + MOVD $1989, R12 + B callbackasm1(SB) + MOVD $1990, R12 + B callbackasm1(SB) + MOVD $1991, R12 + B callbackasm1(SB) + MOVD $1992, R12 + B callbackasm1(SB) + MOVD $1993, R12 + B callbackasm1(SB) + MOVD $1994, R12 + B callbackasm1(SB) + MOVD $1995, R12 + B callbackasm1(SB) + MOVD $1996, R12 + B callbackasm1(SB) + MOVD $1997, R12 + B callbackasm1(SB) + MOVD $1998, R12 + B callbackasm1(SB) + MOVD $1999, R12 + B callbackasm1(SB) diff --git a/vendor/github.com/ebitengine/purego/zcallback_loong64.s b/vendor/github.com/ebitengine/purego/zcallback_loong64.s new file mode 100644 index 0000000..e20c598 --- /dev/null +++ b/vendor/github.com/ebitengine/purego/zcallback_loong64.s @@ -0,0 +1,4014 @@ +// Code generated by wincallback.go using 'go generate'. DO NOT EDIT. + +//go:build darwin || freebsd || linux || netbsd + +// External code calls into callbackasm at an offset corresponding +// to the callback index. Callbackasm is a table of MOVV and JMP instructions. +// The MOVV instruction loads R12 with the callback index, and the +// JMP instruction branches to callbackasm1. +// callbackasm1 takes the callback index from R12 and +// indexes into an array that stores information about each callback. +// It then calls the Go implementation for that callback. +#include "textflag.h" + +TEXT callbackasm(SB),NOSPLIT|NOFRAME,$0 + MOVV $0, R12 + JMP callbackasm1(SB) + MOVV $1, R12 + JMP callbackasm1(SB) + MOVV $2, R12 + JMP callbackasm1(SB) + MOVV $3, R12 + JMP callbackasm1(SB) + MOVV $4, R12 + JMP callbackasm1(SB) + MOVV $5, R12 + JMP callbackasm1(SB) + MOVV $6, R12 + JMP callbackasm1(SB) + MOVV $7, R12 + JMP callbackasm1(SB) + MOVV $8, R12 + JMP callbackasm1(SB) + MOVV $9, R12 + JMP callbackasm1(SB) + MOVV $10, R12 + JMP callbackasm1(SB) + MOVV $11, R12 + JMP callbackasm1(SB) + MOVV $12, R12 + JMP callbackasm1(SB) + MOVV $13, R12 + JMP callbackasm1(SB) + MOVV $14, R12 + JMP callbackasm1(SB) + MOVV $15, R12 + JMP callbackasm1(SB) + MOVV $16, R12 + JMP callbackasm1(SB) + MOVV $17, R12 + JMP callbackasm1(SB) + MOVV $18, R12 + JMP callbackasm1(SB) + MOVV $19, R12 + JMP callbackasm1(SB) + MOVV $20, R12 + JMP callbackasm1(SB) + MOVV $21, R12 + JMP callbackasm1(SB) + MOVV $22, R12 + JMP callbackasm1(SB) + MOVV $23, R12 + JMP callbackasm1(SB) + MOVV $24, R12 + JMP callbackasm1(SB) + MOVV $25, R12 + JMP callbackasm1(SB) + MOVV $26, R12 + JMP callbackasm1(SB) + MOVV $27, R12 + JMP callbackasm1(SB) + MOVV $28, R12 + JMP callbackasm1(SB) + MOVV $29, R12 + JMP callbackasm1(SB) + MOVV $30, R12 + JMP callbackasm1(SB) + MOVV $31, R12 + JMP callbackasm1(SB) + MOVV $32, R12 + JMP callbackasm1(SB) + MOVV $33, R12 + JMP callbackasm1(SB) + MOVV $34, R12 + JMP callbackasm1(SB) + MOVV $35, R12 + JMP callbackasm1(SB) + MOVV $36, R12 + JMP callbackasm1(SB) + MOVV $37, R12 + JMP callbackasm1(SB) + MOVV $38, R12 + JMP callbackasm1(SB) + MOVV $39, R12 + JMP callbackasm1(SB) + MOVV $40, R12 + JMP callbackasm1(SB) + MOVV $41, R12 + JMP callbackasm1(SB) + MOVV $42, R12 + JMP callbackasm1(SB) + MOVV $43, R12 + JMP callbackasm1(SB) + MOVV $44, R12 + JMP callbackasm1(SB) + MOVV $45, R12 + JMP callbackasm1(SB) + MOVV $46, R12 + JMP callbackasm1(SB) + MOVV $47, R12 + JMP callbackasm1(SB) + MOVV $48, R12 + JMP callbackasm1(SB) + MOVV $49, R12 + JMP callbackasm1(SB) + MOVV $50, R12 + JMP callbackasm1(SB) + MOVV $51, R12 + JMP callbackasm1(SB) + MOVV $52, R12 + JMP callbackasm1(SB) + MOVV $53, R12 + JMP callbackasm1(SB) + MOVV $54, R12 + JMP callbackasm1(SB) + MOVV $55, R12 + JMP callbackasm1(SB) + MOVV $56, R12 + JMP callbackasm1(SB) + MOVV $57, R12 + JMP callbackasm1(SB) + MOVV $58, R12 + JMP callbackasm1(SB) + MOVV $59, R12 + JMP callbackasm1(SB) + MOVV $60, R12 + JMP callbackasm1(SB) + MOVV $61, R12 + JMP callbackasm1(SB) + MOVV $62, R12 + JMP callbackasm1(SB) + MOVV $63, R12 + JMP callbackasm1(SB) + MOVV $64, R12 + JMP callbackasm1(SB) + MOVV $65, R12 + JMP callbackasm1(SB) + MOVV $66, R12 + JMP callbackasm1(SB) + MOVV $67, R12 + JMP callbackasm1(SB) + MOVV $68, R12 + JMP callbackasm1(SB) + MOVV $69, R12 + JMP callbackasm1(SB) + MOVV $70, R12 + JMP callbackasm1(SB) + MOVV $71, R12 + JMP callbackasm1(SB) + MOVV $72, R12 + JMP callbackasm1(SB) + MOVV $73, R12 + JMP callbackasm1(SB) + MOVV $74, R12 + JMP callbackasm1(SB) + MOVV $75, R12 + JMP callbackasm1(SB) + MOVV $76, R12 + JMP callbackasm1(SB) + MOVV $77, R12 + JMP callbackasm1(SB) + MOVV $78, R12 + JMP callbackasm1(SB) + MOVV $79, R12 + JMP callbackasm1(SB) + MOVV $80, R12 + JMP callbackasm1(SB) + MOVV $81, R12 + JMP callbackasm1(SB) + MOVV $82, R12 + JMP callbackasm1(SB) + MOVV $83, R12 + JMP callbackasm1(SB) + MOVV $84, R12 + JMP callbackasm1(SB) + MOVV $85, R12 + JMP callbackasm1(SB) + MOVV $86, R12 + JMP callbackasm1(SB) + MOVV $87, R12 + JMP callbackasm1(SB) + MOVV $88, R12 + JMP callbackasm1(SB) + MOVV $89, R12 + JMP callbackasm1(SB) + MOVV $90, R12 + JMP callbackasm1(SB) + MOVV $91, R12 + JMP callbackasm1(SB) + MOVV $92, R12 + JMP callbackasm1(SB) + MOVV $93, R12 + JMP callbackasm1(SB) + MOVV $94, R12 + JMP callbackasm1(SB) + MOVV $95, R12 + JMP callbackasm1(SB) + MOVV $96, R12 + JMP callbackasm1(SB) + MOVV $97, R12 + JMP callbackasm1(SB) + MOVV $98, R12 + JMP callbackasm1(SB) + MOVV $99, R12 + JMP callbackasm1(SB) + MOVV $100, R12 + JMP callbackasm1(SB) + MOVV $101, R12 + JMP callbackasm1(SB) + MOVV $102, R12 + JMP callbackasm1(SB) + MOVV $103, R12 + JMP callbackasm1(SB) + MOVV $104, R12 + JMP callbackasm1(SB) + MOVV $105, R12 + JMP callbackasm1(SB) + MOVV $106, R12 + JMP callbackasm1(SB) + MOVV $107, R12 + JMP callbackasm1(SB) + MOVV $108, R12 + JMP callbackasm1(SB) + MOVV $109, R12 + JMP callbackasm1(SB) + MOVV $110, R12 + JMP callbackasm1(SB) + MOVV $111, R12 + JMP callbackasm1(SB) + MOVV $112, R12 + JMP callbackasm1(SB) + MOVV $113, R12 + JMP callbackasm1(SB) + MOVV $114, R12 + JMP callbackasm1(SB) + MOVV $115, R12 + JMP callbackasm1(SB) + MOVV $116, R12 + JMP callbackasm1(SB) + MOVV $117, R12 + JMP callbackasm1(SB) + MOVV $118, R12 + JMP callbackasm1(SB) + MOVV $119, R12 + JMP callbackasm1(SB) + MOVV $120, R12 + JMP callbackasm1(SB) + MOVV $121, R12 + JMP callbackasm1(SB) + MOVV $122, R12 + JMP callbackasm1(SB) + MOVV $123, R12 + JMP callbackasm1(SB) + MOVV $124, R12 + JMP callbackasm1(SB) + MOVV $125, R12 + JMP callbackasm1(SB) + MOVV $126, R12 + JMP callbackasm1(SB) + MOVV $127, R12 + JMP callbackasm1(SB) + MOVV $128, R12 + JMP callbackasm1(SB) + MOVV $129, R12 + JMP callbackasm1(SB) + MOVV $130, R12 + JMP callbackasm1(SB) + MOVV $131, R12 + JMP callbackasm1(SB) + MOVV $132, R12 + JMP callbackasm1(SB) + MOVV $133, R12 + JMP callbackasm1(SB) + MOVV $134, R12 + JMP callbackasm1(SB) + MOVV $135, R12 + JMP callbackasm1(SB) + MOVV $136, R12 + JMP callbackasm1(SB) + MOVV $137, R12 + JMP callbackasm1(SB) + MOVV $138, R12 + JMP callbackasm1(SB) + MOVV $139, R12 + JMP callbackasm1(SB) + MOVV $140, R12 + JMP callbackasm1(SB) + MOVV $141, R12 + JMP callbackasm1(SB) + MOVV $142, R12 + JMP callbackasm1(SB) + MOVV $143, R12 + JMP callbackasm1(SB) + MOVV $144, R12 + JMP callbackasm1(SB) + MOVV $145, R12 + JMP callbackasm1(SB) + MOVV $146, R12 + JMP callbackasm1(SB) + MOVV $147, R12 + JMP callbackasm1(SB) + MOVV $148, R12 + JMP callbackasm1(SB) + MOVV $149, R12 + JMP callbackasm1(SB) + MOVV $150, R12 + JMP callbackasm1(SB) + MOVV $151, R12 + JMP callbackasm1(SB) + MOVV $152, R12 + JMP callbackasm1(SB) + MOVV $153, R12 + JMP callbackasm1(SB) + MOVV $154, R12 + JMP callbackasm1(SB) + MOVV $155, R12 + JMP callbackasm1(SB) + MOVV $156, R12 + JMP callbackasm1(SB) + MOVV $157, R12 + JMP callbackasm1(SB) + MOVV $158, R12 + JMP callbackasm1(SB) + MOVV $159, R12 + JMP callbackasm1(SB) + MOVV $160, R12 + JMP callbackasm1(SB) + MOVV $161, R12 + JMP callbackasm1(SB) + MOVV $162, R12 + JMP callbackasm1(SB) + MOVV $163, R12 + JMP callbackasm1(SB) + MOVV $164, R12 + JMP callbackasm1(SB) + MOVV $165, R12 + JMP callbackasm1(SB) + MOVV $166, R12 + JMP callbackasm1(SB) + MOVV $167, R12 + JMP callbackasm1(SB) + MOVV $168, R12 + JMP callbackasm1(SB) + MOVV $169, R12 + JMP callbackasm1(SB) + MOVV $170, R12 + JMP callbackasm1(SB) + MOVV $171, R12 + JMP callbackasm1(SB) + MOVV $172, R12 + JMP callbackasm1(SB) + MOVV $173, R12 + JMP callbackasm1(SB) + MOVV $174, R12 + JMP callbackasm1(SB) + MOVV $175, R12 + JMP callbackasm1(SB) + MOVV $176, R12 + JMP callbackasm1(SB) + MOVV $177, R12 + JMP callbackasm1(SB) + MOVV $178, R12 + JMP callbackasm1(SB) + MOVV $179, R12 + JMP callbackasm1(SB) + MOVV $180, R12 + JMP callbackasm1(SB) + MOVV $181, R12 + JMP callbackasm1(SB) + MOVV $182, R12 + JMP callbackasm1(SB) + MOVV $183, R12 + JMP callbackasm1(SB) + MOVV $184, R12 + JMP callbackasm1(SB) + MOVV $185, R12 + JMP callbackasm1(SB) + MOVV $186, R12 + JMP callbackasm1(SB) + MOVV $187, R12 + JMP callbackasm1(SB) + MOVV $188, R12 + JMP callbackasm1(SB) + MOVV $189, R12 + JMP callbackasm1(SB) + MOVV $190, R12 + JMP callbackasm1(SB) + MOVV $191, R12 + JMP callbackasm1(SB) + MOVV $192, R12 + JMP callbackasm1(SB) + MOVV $193, R12 + JMP callbackasm1(SB) + MOVV $194, R12 + JMP callbackasm1(SB) + MOVV $195, R12 + JMP callbackasm1(SB) + MOVV $196, R12 + JMP callbackasm1(SB) + MOVV $197, R12 + JMP callbackasm1(SB) + MOVV $198, R12 + JMP callbackasm1(SB) + MOVV $199, R12 + JMP callbackasm1(SB) + MOVV $200, R12 + JMP callbackasm1(SB) + MOVV $201, R12 + JMP callbackasm1(SB) + MOVV $202, R12 + JMP callbackasm1(SB) + MOVV $203, R12 + JMP callbackasm1(SB) + MOVV $204, R12 + JMP callbackasm1(SB) + MOVV $205, R12 + JMP callbackasm1(SB) + MOVV $206, R12 + JMP callbackasm1(SB) + MOVV $207, R12 + JMP callbackasm1(SB) + MOVV $208, R12 + JMP callbackasm1(SB) + MOVV $209, R12 + JMP callbackasm1(SB) + MOVV $210, R12 + JMP callbackasm1(SB) + MOVV $211, R12 + JMP callbackasm1(SB) + MOVV $212, R12 + JMP callbackasm1(SB) + MOVV $213, R12 + JMP callbackasm1(SB) + MOVV $214, R12 + JMP callbackasm1(SB) + MOVV $215, R12 + JMP callbackasm1(SB) + MOVV $216, R12 + JMP callbackasm1(SB) + MOVV $217, R12 + JMP callbackasm1(SB) + MOVV $218, R12 + JMP callbackasm1(SB) + MOVV $219, R12 + JMP callbackasm1(SB) + MOVV $220, R12 + JMP callbackasm1(SB) + MOVV $221, R12 + JMP callbackasm1(SB) + MOVV $222, R12 + JMP callbackasm1(SB) + MOVV $223, R12 + JMP callbackasm1(SB) + MOVV $224, R12 + JMP callbackasm1(SB) + MOVV $225, R12 + JMP callbackasm1(SB) + MOVV $226, R12 + JMP callbackasm1(SB) + MOVV $227, R12 + JMP callbackasm1(SB) + MOVV $228, R12 + JMP callbackasm1(SB) + MOVV $229, R12 + JMP callbackasm1(SB) + MOVV $230, R12 + JMP callbackasm1(SB) + MOVV $231, R12 + JMP callbackasm1(SB) + MOVV $232, R12 + JMP callbackasm1(SB) + MOVV $233, R12 + JMP callbackasm1(SB) + MOVV $234, R12 + JMP callbackasm1(SB) + MOVV $235, R12 + JMP callbackasm1(SB) + MOVV $236, R12 + JMP callbackasm1(SB) + MOVV $237, R12 + JMP callbackasm1(SB) + MOVV $238, R12 + JMP callbackasm1(SB) + MOVV $239, R12 + JMP callbackasm1(SB) + MOVV $240, R12 + JMP callbackasm1(SB) + MOVV $241, R12 + JMP callbackasm1(SB) + MOVV $242, R12 + JMP callbackasm1(SB) + MOVV $243, R12 + JMP callbackasm1(SB) + MOVV $244, R12 + JMP callbackasm1(SB) + MOVV $245, R12 + JMP callbackasm1(SB) + MOVV $246, R12 + JMP callbackasm1(SB) + MOVV $247, R12 + JMP callbackasm1(SB) + MOVV $248, R12 + JMP callbackasm1(SB) + MOVV $249, R12 + JMP callbackasm1(SB) + MOVV $250, R12 + JMP callbackasm1(SB) + MOVV $251, R12 + JMP callbackasm1(SB) + MOVV $252, R12 + JMP callbackasm1(SB) + MOVV $253, R12 + JMP callbackasm1(SB) + MOVV $254, R12 + JMP callbackasm1(SB) + MOVV $255, R12 + JMP callbackasm1(SB) + MOVV $256, R12 + JMP callbackasm1(SB) + MOVV $257, R12 + JMP callbackasm1(SB) + MOVV $258, R12 + JMP callbackasm1(SB) + MOVV $259, R12 + JMP callbackasm1(SB) + MOVV $260, R12 + JMP callbackasm1(SB) + MOVV $261, R12 + JMP callbackasm1(SB) + MOVV $262, R12 + JMP callbackasm1(SB) + MOVV $263, R12 + JMP callbackasm1(SB) + MOVV $264, R12 + JMP callbackasm1(SB) + MOVV $265, R12 + JMP callbackasm1(SB) + MOVV $266, R12 + JMP callbackasm1(SB) + MOVV $267, R12 + JMP callbackasm1(SB) + MOVV $268, R12 + JMP callbackasm1(SB) + MOVV $269, R12 + JMP callbackasm1(SB) + MOVV $270, R12 + JMP callbackasm1(SB) + MOVV $271, R12 + JMP callbackasm1(SB) + MOVV $272, R12 + JMP callbackasm1(SB) + MOVV $273, R12 + JMP callbackasm1(SB) + MOVV $274, R12 + JMP callbackasm1(SB) + MOVV $275, R12 + JMP callbackasm1(SB) + MOVV $276, R12 + JMP callbackasm1(SB) + MOVV $277, R12 + JMP callbackasm1(SB) + MOVV $278, R12 + JMP callbackasm1(SB) + MOVV $279, R12 + JMP callbackasm1(SB) + MOVV $280, R12 + JMP callbackasm1(SB) + MOVV $281, R12 + JMP callbackasm1(SB) + MOVV $282, R12 + JMP callbackasm1(SB) + MOVV $283, R12 + JMP callbackasm1(SB) + MOVV $284, R12 + JMP callbackasm1(SB) + MOVV $285, R12 + JMP callbackasm1(SB) + MOVV $286, R12 + JMP callbackasm1(SB) + MOVV $287, R12 + JMP callbackasm1(SB) + MOVV $288, R12 + JMP callbackasm1(SB) + MOVV $289, R12 + JMP callbackasm1(SB) + MOVV $290, R12 + JMP callbackasm1(SB) + MOVV $291, R12 + JMP callbackasm1(SB) + MOVV $292, R12 + JMP callbackasm1(SB) + MOVV $293, R12 + JMP callbackasm1(SB) + MOVV $294, R12 + JMP callbackasm1(SB) + MOVV $295, R12 + JMP callbackasm1(SB) + MOVV $296, R12 + JMP callbackasm1(SB) + MOVV $297, R12 + JMP callbackasm1(SB) + MOVV $298, R12 + JMP callbackasm1(SB) + MOVV $299, R12 + JMP callbackasm1(SB) + MOVV $300, R12 + JMP callbackasm1(SB) + MOVV $301, R12 + JMP callbackasm1(SB) + MOVV $302, R12 + JMP callbackasm1(SB) + MOVV $303, R12 + JMP callbackasm1(SB) + MOVV $304, R12 + JMP callbackasm1(SB) + MOVV $305, R12 + JMP callbackasm1(SB) + MOVV $306, R12 + JMP callbackasm1(SB) + MOVV $307, R12 + JMP callbackasm1(SB) + MOVV $308, R12 + JMP callbackasm1(SB) + MOVV $309, R12 + JMP callbackasm1(SB) + MOVV $310, R12 + JMP callbackasm1(SB) + MOVV $311, R12 + JMP callbackasm1(SB) + MOVV $312, R12 + JMP callbackasm1(SB) + MOVV $313, R12 + JMP callbackasm1(SB) + MOVV $314, R12 + JMP callbackasm1(SB) + MOVV $315, R12 + JMP callbackasm1(SB) + MOVV $316, R12 + JMP callbackasm1(SB) + MOVV $317, R12 + JMP callbackasm1(SB) + MOVV $318, R12 + JMP callbackasm1(SB) + MOVV $319, R12 + JMP callbackasm1(SB) + MOVV $320, R12 + JMP callbackasm1(SB) + MOVV $321, R12 + JMP callbackasm1(SB) + MOVV $322, R12 + JMP callbackasm1(SB) + MOVV $323, R12 + JMP callbackasm1(SB) + MOVV $324, R12 + JMP callbackasm1(SB) + MOVV $325, R12 + JMP callbackasm1(SB) + MOVV $326, R12 + JMP callbackasm1(SB) + MOVV $327, R12 + JMP callbackasm1(SB) + MOVV $328, R12 + JMP callbackasm1(SB) + MOVV $329, R12 + JMP callbackasm1(SB) + MOVV $330, R12 + JMP callbackasm1(SB) + MOVV $331, R12 + JMP callbackasm1(SB) + MOVV $332, R12 + JMP callbackasm1(SB) + MOVV $333, R12 + JMP callbackasm1(SB) + MOVV $334, R12 + JMP callbackasm1(SB) + MOVV $335, R12 + JMP callbackasm1(SB) + MOVV $336, R12 + JMP callbackasm1(SB) + MOVV $337, R12 + JMP callbackasm1(SB) + MOVV $338, R12 + JMP callbackasm1(SB) + MOVV $339, R12 + JMP callbackasm1(SB) + MOVV $340, R12 + JMP callbackasm1(SB) + MOVV $341, R12 + JMP callbackasm1(SB) + MOVV $342, R12 + JMP callbackasm1(SB) + MOVV $343, R12 + JMP callbackasm1(SB) + MOVV $344, R12 + JMP callbackasm1(SB) + MOVV $345, R12 + JMP callbackasm1(SB) + MOVV $346, R12 + JMP callbackasm1(SB) + MOVV $347, R12 + JMP callbackasm1(SB) + MOVV $348, R12 + JMP callbackasm1(SB) + MOVV $349, R12 + JMP callbackasm1(SB) + MOVV $350, R12 + JMP callbackasm1(SB) + MOVV $351, R12 + JMP callbackasm1(SB) + MOVV $352, R12 + JMP callbackasm1(SB) + MOVV $353, R12 + JMP callbackasm1(SB) + MOVV $354, R12 + JMP callbackasm1(SB) + MOVV $355, R12 + JMP callbackasm1(SB) + MOVV $356, R12 + JMP callbackasm1(SB) + MOVV $357, R12 + JMP callbackasm1(SB) + MOVV $358, R12 + JMP callbackasm1(SB) + MOVV $359, R12 + JMP callbackasm1(SB) + MOVV $360, R12 + JMP callbackasm1(SB) + MOVV $361, R12 + JMP callbackasm1(SB) + MOVV $362, R12 + JMP callbackasm1(SB) + MOVV $363, R12 + JMP callbackasm1(SB) + MOVV $364, R12 + JMP callbackasm1(SB) + MOVV $365, R12 + JMP callbackasm1(SB) + MOVV $366, R12 + JMP callbackasm1(SB) + MOVV $367, R12 + JMP callbackasm1(SB) + MOVV $368, R12 + JMP callbackasm1(SB) + MOVV $369, R12 + JMP callbackasm1(SB) + MOVV $370, R12 + JMP callbackasm1(SB) + MOVV $371, R12 + JMP callbackasm1(SB) + MOVV $372, R12 + JMP callbackasm1(SB) + MOVV $373, R12 + JMP callbackasm1(SB) + MOVV $374, R12 + JMP callbackasm1(SB) + MOVV $375, R12 + JMP callbackasm1(SB) + MOVV $376, R12 + JMP callbackasm1(SB) + MOVV $377, R12 + JMP callbackasm1(SB) + MOVV $378, R12 + JMP callbackasm1(SB) + MOVV $379, R12 + JMP callbackasm1(SB) + MOVV $380, R12 + JMP callbackasm1(SB) + MOVV $381, R12 + JMP callbackasm1(SB) + MOVV $382, R12 + JMP callbackasm1(SB) + MOVV $383, R12 + JMP callbackasm1(SB) + MOVV $384, R12 + JMP callbackasm1(SB) + MOVV $385, R12 + JMP callbackasm1(SB) + MOVV $386, R12 + JMP callbackasm1(SB) + MOVV $387, R12 + JMP callbackasm1(SB) + MOVV $388, R12 + JMP callbackasm1(SB) + MOVV $389, R12 + JMP callbackasm1(SB) + MOVV $390, R12 + JMP callbackasm1(SB) + MOVV $391, R12 + JMP callbackasm1(SB) + MOVV $392, R12 + JMP callbackasm1(SB) + MOVV $393, R12 + JMP callbackasm1(SB) + MOVV $394, R12 + JMP callbackasm1(SB) + MOVV $395, R12 + JMP callbackasm1(SB) + MOVV $396, R12 + JMP callbackasm1(SB) + MOVV $397, R12 + JMP callbackasm1(SB) + MOVV $398, R12 + JMP callbackasm1(SB) + MOVV $399, R12 + JMP callbackasm1(SB) + MOVV $400, R12 + JMP callbackasm1(SB) + MOVV $401, R12 + JMP callbackasm1(SB) + MOVV $402, R12 + JMP callbackasm1(SB) + MOVV $403, R12 + JMP callbackasm1(SB) + MOVV $404, R12 + JMP callbackasm1(SB) + MOVV $405, R12 + JMP callbackasm1(SB) + MOVV $406, R12 + JMP callbackasm1(SB) + MOVV $407, R12 + JMP callbackasm1(SB) + MOVV $408, R12 + JMP callbackasm1(SB) + MOVV $409, R12 + JMP callbackasm1(SB) + MOVV $410, R12 + JMP callbackasm1(SB) + MOVV $411, R12 + JMP callbackasm1(SB) + MOVV $412, R12 + JMP callbackasm1(SB) + MOVV $413, R12 + JMP callbackasm1(SB) + MOVV $414, R12 + JMP callbackasm1(SB) + MOVV $415, R12 + JMP callbackasm1(SB) + MOVV $416, R12 + JMP callbackasm1(SB) + MOVV $417, R12 + JMP callbackasm1(SB) + MOVV $418, R12 + JMP callbackasm1(SB) + MOVV $419, R12 + JMP callbackasm1(SB) + MOVV $420, R12 + JMP callbackasm1(SB) + MOVV $421, R12 + JMP callbackasm1(SB) + MOVV $422, R12 + JMP callbackasm1(SB) + MOVV $423, R12 + JMP callbackasm1(SB) + MOVV $424, R12 + JMP callbackasm1(SB) + MOVV $425, R12 + JMP callbackasm1(SB) + MOVV $426, R12 + JMP callbackasm1(SB) + MOVV $427, R12 + JMP callbackasm1(SB) + MOVV $428, R12 + JMP callbackasm1(SB) + MOVV $429, R12 + JMP callbackasm1(SB) + MOVV $430, R12 + JMP callbackasm1(SB) + MOVV $431, R12 + JMP callbackasm1(SB) + MOVV $432, R12 + JMP callbackasm1(SB) + MOVV $433, R12 + JMP callbackasm1(SB) + MOVV $434, R12 + JMP callbackasm1(SB) + MOVV $435, R12 + JMP callbackasm1(SB) + MOVV $436, R12 + JMP callbackasm1(SB) + MOVV $437, R12 + JMP callbackasm1(SB) + MOVV $438, R12 + JMP callbackasm1(SB) + MOVV $439, R12 + JMP callbackasm1(SB) + MOVV $440, R12 + JMP callbackasm1(SB) + MOVV $441, R12 + JMP callbackasm1(SB) + MOVV $442, R12 + JMP callbackasm1(SB) + MOVV $443, R12 + JMP callbackasm1(SB) + MOVV $444, R12 + JMP callbackasm1(SB) + MOVV $445, R12 + JMP callbackasm1(SB) + MOVV $446, R12 + JMP callbackasm1(SB) + MOVV $447, R12 + JMP callbackasm1(SB) + MOVV $448, R12 + JMP callbackasm1(SB) + MOVV $449, R12 + JMP callbackasm1(SB) + MOVV $450, R12 + JMP callbackasm1(SB) + MOVV $451, R12 + JMP callbackasm1(SB) + MOVV $452, R12 + JMP callbackasm1(SB) + MOVV $453, R12 + JMP callbackasm1(SB) + MOVV $454, R12 + JMP callbackasm1(SB) + MOVV $455, R12 + JMP callbackasm1(SB) + MOVV $456, R12 + JMP callbackasm1(SB) + MOVV $457, R12 + JMP callbackasm1(SB) + MOVV $458, R12 + JMP callbackasm1(SB) + MOVV $459, R12 + JMP callbackasm1(SB) + MOVV $460, R12 + JMP callbackasm1(SB) + MOVV $461, R12 + JMP callbackasm1(SB) + MOVV $462, R12 + JMP callbackasm1(SB) + MOVV $463, R12 + JMP callbackasm1(SB) + MOVV $464, R12 + JMP callbackasm1(SB) + MOVV $465, R12 + JMP callbackasm1(SB) + MOVV $466, R12 + JMP callbackasm1(SB) + MOVV $467, R12 + JMP callbackasm1(SB) + MOVV $468, R12 + JMP callbackasm1(SB) + MOVV $469, R12 + JMP callbackasm1(SB) + MOVV $470, R12 + JMP callbackasm1(SB) + MOVV $471, R12 + JMP callbackasm1(SB) + MOVV $472, R12 + JMP callbackasm1(SB) + MOVV $473, R12 + JMP callbackasm1(SB) + MOVV $474, R12 + JMP callbackasm1(SB) + MOVV $475, R12 + JMP callbackasm1(SB) + MOVV $476, R12 + JMP callbackasm1(SB) + MOVV $477, R12 + JMP callbackasm1(SB) + MOVV $478, R12 + JMP callbackasm1(SB) + MOVV $479, R12 + JMP callbackasm1(SB) + MOVV $480, R12 + JMP callbackasm1(SB) + MOVV $481, R12 + JMP callbackasm1(SB) + MOVV $482, R12 + JMP callbackasm1(SB) + MOVV $483, R12 + JMP callbackasm1(SB) + MOVV $484, R12 + JMP callbackasm1(SB) + MOVV $485, R12 + JMP callbackasm1(SB) + MOVV $486, R12 + JMP callbackasm1(SB) + MOVV $487, R12 + JMP callbackasm1(SB) + MOVV $488, R12 + JMP callbackasm1(SB) + MOVV $489, R12 + JMP callbackasm1(SB) + MOVV $490, R12 + JMP callbackasm1(SB) + MOVV $491, R12 + JMP callbackasm1(SB) + MOVV $492, R12 + JMP callbackasm1(SB) + MOVV $493, R12 + JMP callbackasm1(SB) + MOVV $494, R12 + JMP callbackasm1(SB) + MOVV $495, R12 + JMP callbackasm1(SB) + MOVV $496, R12 + JMP callbackasm1(SB) + MOVV $497, R12 + JMP callbackasm1(SB) + MOVV $498, R12 + JMP callbackasm1(SB) + MOVV $499, R12 + JMP callbackasm1(SB) + MOVV $500, R12 + JMP callbackasm1(SB) + MOVV $501, R12 + JMP callbackasm1(SB) + MOVV $502, R12 + JMP callbackasm1(SB) + MOVV $503, R12 + JMP callbackasm1(SB) + MOVV $504, R12 + JMP callbackasm1(SB) + MOVV $505, R12 + JMP callbackasm1(SB) + MOVV $506, R12 + JMP callbackasm1(SB) + MOVV $507, R12 + JMP callbackasm1(SB) + MOVV $508, R12 + JMP callbackasm1(SB) + MOVV $509, R12 + JMP callbackasm1(SB) + MOVV $510, R12 + JMP callbackasm1(SB) + MOVV $511, R12 + JMP callbackasm1(SB) + MOVV $512, R12 + JMP callbackasm1(SB) + MOVV $513, R12 + JMP callbackasm1(SB) + MOVV $514, R12 + JMP callbackasm1(SB) + MOVV $515, R12 + JMP callbackasm1(SB) + MOVV $516, R12 + JMP callbackasm1(SB) + MOVV $517, R12 + JMP callbackasm1(SB) + MOVV $518, R12 + JMP callbackasm1(SB) + MOVV $519, R12 + JMP callbackasm1(SB) + MOVV $520, R12 + JMP callbackasm1(SB) + MOVV $521, R12 + JMP callbackasm1(SB) + MOVV $522, R12 + JMP callbackasm1(SB) + MOVV $523, R12 + JMP callbackasm1(SB) + MOVV $524, R12 + JMP callbackasm1(SB) + MOVV $525, R12 + JMP callbackasm1(SB) + MOVV $526, R12 + JMP callbackasm1(SB) + MOVV $527, R12 + JMP callbackasm1(SB) + MOVV $528, R12 + JMP callbackasm1(SB) + MOVV $529, R12 + JMP callbackasm1(SB) + MOVV $530, R12 + JMP callbackasm1(SB) + MOVV $531, R12 + JMP callbackasm1(SB) + MOVV $532, R12 + JMP callbackasm1(SB) + MOVV $533, R12 + JMP callbackasm1(SB) + MOVV $534, R12 + JMP callbackasm1(SB) + MOVV $535, R12 + JMP callbackasm1(SB) + MOVV $536, R12 + JMP callbackasm1(SB) + MOVV $537, R12 + JMP callbackasm1(SB) + MOVV $538, R12 + JMP callbackasm1(SB) + MOVV $539, R12 + JMP callbackasm1(SB) + MOVV $540, R12 + JMP callbackasm1(SB) + MOVV $541, R12 + JMP callbackasm1(SB) + MOVV $542, R12 + JMP callbackasm1(SB) + MOVV $543, R12 + JMP callbackasm1(SB) + MOVV $544, R12 + JMP callbackasm1(SB) + MOVV $545, R12 + JMP callbackasm1(SB) + MOVV $546, R12 + JMP callbackasm1(SB) + MOVV $547, R12 + JMP callbackasm1(SB) + MOVV $548, R12 + JMP callbackasm1(SB) + MOVV $549, R12 + JMP callbackasm1(SB) + MOVV $550, R12 + JMP callbackasm1(SB) + MOVV $551, R12 + JMP callbackasm1(SB) + MOVV $552, R12 + JMP callbackasm1(SB) + MOVV $553, R12 + JMP callbackasm1(SB) + MOVV $554, R12 + JMP callbackasm1(SB) + MOVV $555, R12 + JMP callbackasm1(SB) + MOVV $556, R12 + JMP callbackasm1(SB) + MOVV $557, R12 + JMP callbackasm1(SB) + MOVV $558, R12 + JMP callbackasm1(SB) + MOVV $559, R12 + JMP callbackasm1(SB) + MOVV $560, R12 + JMP callbackasm1(SB) + MOVV $561, R12 + JMP callbackasm1(SB) + MOVV $562, R12 + JMP callbackasm1(SB) + MOVV $563, R12 + JMP callbackasm1(SB) + MOVV $564, R12 + JMP callbackasm1(SB) + MOVV $565, R12 + JMP callbackasm1(SB) + MOVV $566, R12 + JMP callbackasm1(SB) + MOVV $567, R12 + JMP callbackasm1(SB) + MOVV $568, R12 + JMP callbackasm1(SB) + MOVV $569, R12 + JMP callbackasm1(SB) + MOVV $570, R12 + JMP callbackasm1(SB) + MOVV $571, R12 + JMP callbackasm1(SB) + MOVV $572, R12 + JMP callbackasm1(SB) + MOVV $573, R12 + JMP callbackasm1(SB) + MOVV $574, R12 + JMP callbackasm1(SB) + MOVV $575, R12 + JMP callbackasm1(SB) + MOVV $576, R12 + JMP callbackasm1(SB) + MOVV $577, R12 + JMP callbackasm1(SB) + MOVV $578, R12 + JMP callbackasm1(SB) + MOVV $579, R12 + JMP callbackasm1(SB) + MOVV $580, R12 + JMP callbackasm1(SB) + MOVV $581, R12 + JMP callbackasm1(SB) + MOVV $582, R12 + JMP callbackasm1(SB) + MOVV $583, R12 + JMP callbackasm1(SB) + MOVV $584, R12 + JMP callbackasm1(SB) + MOVV $585, R12 + JMP callbackasm1(SB) + MOVV $586, R12 + JMP callbackasm1(SB) + MOVV $587, R12 + JMP callbackasm1(SB) + MOVV $588, R12 + JMP callbackasm1(SB) + MOVV $589, R12 + JMP callbackasm1(SB) + MOVV $590, R12 + JMP callbackasm1(SB) + MOVV $591, R12 + JMP callbackasm1(SB) + MOVV $592, R12 + JMP callbackasm1(SB) + MOVV $593, R12 + JMP callbackasm1(SB) + MOVV $594, R12 + JMP callbackasm1(SB) + MOVV $595, R12 + JMP callbackasm1(SB) + MOVV $596, R12 + JMP callbackasm1(SB) + MOVV $597, R12 + JMP callbackasm1(SB) + MOVV $598, R12 + JMP callbackasm1(SB) + MOVV $599, R12 + JMP callbackasm1(SB) + MOVV $600, R12 + JMP callbackasm1(SB) + MOVV $601, R12 + JMP callbackasm1(SB) + MOVV $602, R12 + JMP callbackasm1(SB) + MOVV $603, R12 + JMP callbackasm1(SB) + MOVV $604, R12 + JMP callbackasm1(SB) + MOVV $605, R12 + JMP callbackasm1(SB) + MOVV $606, R12 + JMP callbackasm1(SB) + MOVV $607, R12 + JMP callbackasm1(SB) + MOVV $608, R12 + JMP callbackasm1(SB) + MOVV $609, R12 + JMP callbackasm1(SB) + MOVV $610, R12 + JMP callbackasm1(SB) + MOVV $611, R12 + JMP callbackasm1(SB) + MOVV $612, R12 + JMP callbackasm1(SB) + MOVV $613, R12 + JMP callbackasm1(SB) + MOVV $614, R12 + JMP callbackasm1(SB) + MOVV $615, R12 + JMP callbackasm1(SB) + MOVV $616, R12 + JMP callbackasm1(SB) + MOVV $617, R12 + JMP callbackasm1(SB) + MOVV $618, R12 + JMP callbackasm1(SB) + MOVV $619, R12 + JMP callbackasm1(SB) + MOVV $620, R12 + JMP callbackasm1(SB) + MOVV $621, R12 + JMP callbackasm1(SB) + MOVV $622, R12 + JMP callbackasm1(SB) + MOVV $623, R12 + JMP callbackasm1(SB) + MOVV $624, R12 + JMP callbackasm1(SB) + MOVV $625, R12 + JMP callbackasm1(SB) + MOVV $626, R12 + JMP callbackasm1(SB) + MOVV $627, R12 + JMP callbackasm1(SB) + MOVV $628, R12 + JMP callbackasm1(SB) + MOVV $629, R12 + JMP callbackasm1(SB) + MOVV $630, R12 + JMP callbackasm1(SB) + MOVV $631, R12 + JMP callbackasm1(SB) + MOVV $632, R12 + JMP callbackasm1(SB) + MOVV $633, R12 + JMP callbackasm1(SB) + MOVV $634, R12 + JMP callbackasm1(SB) + MOVV $635, R12 + JMP callbackasm1(SB) + MOVV $636, R12 + JMP callbackasm1(SB) + MOVV $637, R12 + JMP callbackasm1(SB) + MOVV $638, R12 + JMP callbackasm1(SB) + MOVV $639, R12 + JMP callbackasm1(SB) + MOVV $640, R12 + JMP callbackasm1(SB) + MOVV $641, R12 + JMP callbackasm1(SB) + MOVV $642, R12 + JMP callbackasm1(SB) + MOVV $643, R12 + JMP callbackasm1(SB) + MOVV $644, R12 + JMP callbackasm1(SB) + MOVV $645, R12 + JMP callbackasm1(SB) + MOVV $646, R12 + JMP callbackasm1(SB) + MOVV $647, R12 + JMP callbackasm1(SB) + MOVV $648, R12 + JMP callbackasm1(SB) + MOVV $649, R12 + JMP callbackasm1(SB) + MOVV $650, R12 + JMP callbackasm1(SB) + MOVV $651, R12 + JMP callbackasm1(SB) + MOVV $652, R12 + JMP callbackasm1(SB) + MOVV $653, R12 + JMP callbackasm1(SB) + MOVV $654, R12 + JMP callbackasm1(SB) + MOVV $655, R12 + JMP callbackasm1(SB) + MOVV $656, R12 + JMP callbackasm1(SB) + MOVV $657, R12 + JMP callbackasm1(SB) + MOVV $658, R12 + JMP callbackasm1(SB) + MOVV $659, R12 + JMP callbackasm1(SB) + MOVV $660, R12 + JMP callbackasm1(SB) + MOVV $661, R12 + JMP callbackasm1(SB) + MOVV $662, R12 + JMP callbackasm1(SB) + MOVV $663, R12 + JMP callbackasm1(SB) + MOVV $664, R12 + JMP callbackasm1(SB) + MOVV $665, R12 + JMP callbackasm1(SB) + MOVV $666, R12 + JMP callbackasm1(SB) + MOVV $667, R12 + JMP callbackasm1(SB) + MOVV $668, R12 + JMP callbackasm1(SB) + MOVV $669, R12 + JMP callbackasm1(SB) + MOVV $670, R12 + JMP callbackasm1(SB) + MOVV $671, R12 + JMP callbackasm1(SB) + MOVV $672, R12 + JMP callbackasm1(SB) + MOVV $673, R12 + JMP callbackasm1(SB) + MOVV $674, R12 + JMP callbackasm1(SB) + MOVV $675, R12 + JMP callbackasm1(SB) + MOVV $676, R12 + JMP callbackasm1(SB) + MOVV $677, R12 + JMP callbackasm1(SB) + MOVV $678, R12 + JMP callbackasm1(SB) + MOVV $679, R12 + JMP callbackasm1(SB) + MOVV $680, R12 + JMP callbackasm1(SB) + MOVV $681, R12 + JMP callbackasm1(SB) + MOVV $682, R12 + JMP callbackasm1(SB) + MOVV $683, R12 + JMP callbackasm1(SB) + MOVV $684, R12 + JMP callbackasm1(SB) + MOVV $685, R12 + JMP callbackasm1(SB) + MOVV $686, R12 + JMP callbackasm1(SB) + MOVV $687, R12 + JMP callbackasm1(SB) + MOVV $688, R12 + JMP callbackasm1(SB) + MOVV $689, R12 + JMP callbackasm1(SB) + MOVV $690, R12 + JMP callbackasm1(SB) + MOVV $691, R12 + JMP callbackasm1(SB) + MOVV $692, R12 + JMP callbackasm1(SB) + MOVV $693, R12 + JMP callbackasm1(SB) + MOVV $694, R12 + JMP callbackasm1(SB) + MOVV $695, R12 + JMP callbackasm1(SB) + MOVV $696, R12 + JMP callbackasm1(SB) + MOVV $697, R12 + JMP callbackasm1(SB) + MOVV $698, R12 + JMP callbackasm1(SB) + MOVV $699, R12 + JMP callbackasm1(SB) + MOVV $700, R12 + JMP callbackasm1(SB) + MOVV $701, R12 + JMP callbackasm1(SB) + MOVV $702, R12 + JMP callbackasm1(SB) + MOVV $703, R12 + JMP callbackasm1(SB) + MOVV $704, R12 + JMP callbackasm1(SB) + MOVV $705, R12 + JMP callbackasm1(SB) + MOVV $706, R12 + JMP callbackasm1(SB) + MOVV $707, R12 + JMP callbackasm1(SB) + MOVV $708, R12 + JMP callbackasm1(SB) + MOVV $709, R12 + JMP callbackasm1(SB) + MOVV $710, R12 + JMP callbackasm1(SB) + MOVV $711, R12 + JMP callbackasm1(SB) + MOVV $712, R12 + JMP callbackasm1(SB) + MOVV $713, R12 + JMP callbackasm1(SB) + MOVV $714, R12 + JMP callbackasm1(SB) + MOVV $715, R12 + JMP callbackasm1(SB) + MOVV $716, R12 + JMP callbackasm1(SB) + MOVV $717, R12 + JMP callbackasm1(SB) + MOVV $718, R12 + JMP callbackasm1(SB) + MOVV $719, R12 + JMP callbackasm1(SB) + MOVV $720, R12 + JMP callbackasm1(SB) + MOVV $721, R12 + JMP callbackasm1(SB) + MOVV $722, R12 + JMP callbackasm1(SB) + MOVV $723, R12 + JMP callbackasm1(SB) + MOVV $724, R12 + JMP callbackasm1(SB) + MOVV $725, R12 + JMP callbackasm1(SB) + MOVV $726, R12 + JMP callbackasm1(SB) + MOVV $727, R12 + JMP callbackasm1(SB) + MOVV $728, R12 + JMP callbackasm1(SB) + MOVV $729, R12 + JMP callbackasm1(SB) + MOVV $730, R12 + JMP callbackasm1(SB) + MOVV $731, R12 + JMP callbackasm1(SB) + MOVV $732, R12 + JMP callbackasm1(SB) + MOVV $733, R12 + JMP callbackasm1(SB) + MOVV $734, R12 + JMP callbackasm1(SB) + MOVV $735, R12 + JMP callbackasm1(SB) + MOVV $736, R12 + JMP callbackasm1(SB) + MOVV $737, R12 + JMP callbackasm1(SB) + MOVV $738, R12 + JMP callbackasm1(SB) + MOVV $739, R12 + JMP callbackasm1(SB) + MOVV $740, R12 + JMP callbackasm1(SB) + MOVV $741, R12 + JMP callbackasm1(SB) + MOVV $742, R12 + JMP callbackasm1(SB) + MOVV $743, R12 + JMP callbackasm1(SB) + MOVV $744, R12 + JMP callbackasm1(SB) + MOVV $745, R12 + JMP callbackasm1(SB) + MOVV $746, R12 + JMP callbackasm1(SB) + MOVV $747, R12 + JMP callbackasm1(SB) + MOVV $748, R12 + JMP callbackasm1(SB) + MOVV $749, R12 + JMP callbackasm1(SB) + MOVV $750, R12 + JMP callbackasm1(SB) + MOVV $751, R12 + JMP callbackasm1(SB) + MOVV $752, R12 + JMP callbackasm1(SB) + MOVV $753, R12 + JMP callbackasm1(SB) + MOVV $754, R12 + JMP callbackasm1(SB) + MOVV $755, R12 + JMP callbackasm1(SB) + MOVV $756, R12 + JMP callbackasm1(SB) + MOVV $757, R12 + JMP callbackasm1(SB) + MOVV $758, R12 + JMP callbackasm1(SB) + MOVV $759, R12 + JMP callbackasm1(SB) + MOVV $760, R12 + JMP callbackasm1(SB) + MOVV $761, R12 + JMP callbackasm1(SB) + MOVV $762, R12 + JMP callbackasm1(SB) + MOVV $763, R12 + JMP callbackasm1(SB) + MOVV $764, R12 + JMP callbackasm1(SB) + MOVV $765, R12 + JMP callbackasm1(SB) + MOVV $766, R12 + JMP callbackasm1(SB) + MOVV $767, R12 + JMP callbackasm1(SB) + MOVV $768, R12 + JMP callbackasm1(SB) + MOVV $769, R12 + JMP callbackasm1(SB) + MOVV $770, R12 + JMP callbackasm1(SB) + MOVV $771, R12 + JMP callbackasm1(SB) + MOVV $772, R12 + JMP callbackasm1(SB) + MOVV $773, R12 + JMP callbackasm1(SB) + MOVV $774, R12 + JMP callbackasm1(SB) + MOVV $775, R12 + JMP callbackasm1(SB) + MOVV $776, R12 + JMP callbackasm1(SB) + MOVV $777, R12 + JMP callbackasm1(SB) + MOVV $778, R12 + JMP callbackasm1(SB) + MOVV $779, R12 + JMP callbackasm1(SB) + MOVV $780, R12 + JMP callbackasm1(SB) + MOVV $781, R12 + JMP callbackasm1(SB) + MOVV $782, R12 + JMP callbackasm1(SB) + MOVV $783, R12 + JMP callbackasm1(SB) + MOVV $784, R12 + JMP callbackasm1(SB) + MOVV $785, R12 + JMP callbackasm1(SB) + MOVV $786, R12 + JMP callbackasm1(SB) + MOVV $787, R12 + JMP callbackasm1(SB) + MOVV $788, R12 + JMP callbackasm1(SB) + MOVV $789, R12 + JMP callbackasm1(SB) + MOVV $790, R12 + JMP callbackasm1(SB) + MOVV $791, R12 + JMP callbackasm1(SB) + MOVV $792, R12 + JMP callbackasm1(SB) + MOVV $793, R12 + JMP callbackasm1(SB) + MOVV $794, R12 + JMP callbackasm1(SB) + MOVV $795, R12 + JMP callbackasm1(SB) + MOVV $796, R12 + JMP callbackasm1(SB) + MOVV $797, R12 + JMP callbackasm1(SB) + MOVV $798, R12 + JMP callbackasm1(SB) + MOVV $799, R12 + JMP callbackasm1(SB) + MOVV $800, R12 + JMP callbackasm1(SB) + MOVV $801, R12 + JMP callbackasm1(SB) + MOVV $802, R12 + JMP callbackasm1(SB) + MOVV $803, R12 + JMP callbackasm1(SB) + MOVV $804, R12 + JMP callbackasm1(SB) + MOVV $805, R12 + JMP callbackasm1(SB) + MOVV $806, R12 + JMP callbackasm1(SB) + MOVV $807, R12 + JMP callbackasm1(SB) + MOVV $808, R12 + JMP callbackasm1(SB) + MOVV $809, R12 + JMP callbackasm1(SB) + MOVV $810, R12 + JMP callbackasm1(SB) + MOVV $811, R12 + JMP callbackasm1(SB) + MOVV $812, R12 + JMP callbackasm1(SB) + MOVV $813, R12 + JMP callbackasm1(SB) + MOVV $814, R12 + JMP callbackasm1(SB) + MOVV $815, R12 + JMP callbackasm1(SB) + MOVV $816, R12 + JMP callbackasm1(SB) + MOVV $817, R12 + JMP callbackasm1(SB) + MOVV $818, R12 + JMP callbackasm1(SB) + MOVV $819, R12 + JMP callbackasm1(SB) + MOVV $820, R12 + JMP callbackasm1(SB) + MOVV $821, R12 + JMP callbackasm1(SB) + MOVV $822, R12 + JMP callbackasm1(SB) + MOVV $823, R12 + JMP callbackasm1(SB) + MOVV $824, R12 + JMP callbackasm1(SB) + MOVV $825, R12 + JMP callbackasm1(SB) + MOVV $826, R12 + JMP callbackasm1(SB) + MOVV $827, R12 + JMP callbackasm1(SB) + MOVV $828, R12 + JMP callbackasm1(SB) + MOVV $829, R12 + JMP callbackasm1(SB) + MOVV $830, R12 + JMP callbackasm1(SB) + MOVV $831, R12 + JMP callbackasm1(SB) + MOVV $832, R12 + JMP callbackasm1(SB) + MOVV $833, R12 + JMP callbackasm1(SB) + MOVV $834, R12 + JMP callbackasm1(SB) + MOVV $835, R12 + JMP callbackasm1(SB) + MOVV $836, R12 + JMP callbackasm1(SB) + MOVV $837, R12 + JMP callbackasm1(SB) + MOVV $838, R12 + JMP callbackasm1(SB) + MOVV $839, R12 + JMP callbackasm1(SB) + MOVV $840, R12 + JMP callbackasm1(SB) + MOVV $841, R12 + JMP callbackasm1(SB) + MOVV $842, R12 + JMP callbackasm1(SB) + MOVV $843, R12 + JMP callbackasm1(SB) + MOVV $844, R12 + JMP callbackasm1(SB) + MOVV $845, R12 + JMP callbackasm1(SB) + MOVV $846, R12 + JMP callbackasm1(SB) + MOVV $847, R12 + JMP callbackasm1(SB) + MOVV $848, R12 + JMP callbackasm1(SB) + MOVV $849, R12 + JMP callbackasm1(SB) + MOVV $850, R12 + JMP callbackasm1(SB) + MOVV $851, R12 + JMP callbackasm1(SB) + MOVV $852, R12 + JMP callbackasm1(SB) + MOVV $853, R12 + JMP callbackasm1(SB) + MOVV $854, R12 + JMP callbackasm1(SB) + MOVV $855, R12 + JMP callbackasm1(SB) + MOVV $856, R12 + JMP callbackasm1(SB) + MOVV $857, R12 + JMP callbackasm1(SB) + MOVV $858, R12 + JMP callbackasm1(SB) + MOVV $859, R12 + JMP callbackasm1(SB) + MOVV $860, R12 + JMP callbackasm1(SB) + MOVV $861, R12 + JMP callbackasm1(SB) + MOVV $862, R12 + JMP callbackasm1(SB) + MOVV $863, R12 + JMP callbackasm1(SB) + MOVV $864, R12 + JMP callbackasm1(SB) + MOVV $865, R12 + JMP callbackasm1(SB) + MOVV $866, R12 + JMP callbackasm1(SB) + MOVV $867, R12 + JMP callbackasm1(SB) + MOVV $868, R12 + JMP callbackasm1(SB) + MOVV $869, R12 + JMP callbackasm1(SB) + MOVV $870, R12 + JMP callbackasm1(SB) + MOVV $871, R12 + JMP callbackasm1(SB) + MOVV $872, R12 + JMP callbackasm1(SB) + MOVV $873, R12 + JMP callbackasm1(SB) + MOVV $874, R12 + JMP callbackasm1(SB) + MOVV $875, R12 + JMP callbackasm1(SB) + MOVV $876, R12 + JMP callbackasm1(SB) + MOVV $877, R12 + JMP callbackasm1(SB) + MOVV $878, R12 + JMP callbackasm1(SB) + MOVV $879, R12 + JMP callbackasm1(SB) + MOVV $880, R12 + JMP callbackasm1(SB) + MOVV $881, R12 + JMP callbackasm1(SB) + MOVV $882, R12 + JMP callbackasm1(SB) + MOVV $883, R12 + JMP callbackasm1(SB) + MOVV $884, R12 + JMP callbackasm1(SB) + MOVV $885, R12 + JMP callbackasm1(SB) + MOVV $886, R12 + JMP callbackasm1(SB) + MOVV $887, R12 + JMP callbackasm1(SB) + MOVV $888, R12 + JMP callbackasm1(SB) + MOVV $889, R12 + JMP callbackasm1(SB) + MOVV $890, R12 + JMP callbackasm1(SB) + MOVV $891, R12 + JMP callbackasm1(SB) + MOVV $892, R12 + JMP callbackasm1(SB) + MOVV $893, R12 + JMP callbackasm1(SB) + MOVV $894, R12 + JMP callbackasm1(SB) + MOVV $895, R12 + JMP callbackasm1(SB) + MOVV $896, R12 + JMP callbackasm1(SB) + MOVV $897, R12 + JMP callbackasm1(SB) + MOVV $898, R12 + JMP callbackasm1(SB) + MOVV $899, R12 + JMP callbackasm1(SB) + MOVV $900, R12 + JMP callbackasm1(SB) + MOVV $901, R12 + JMP callbackasm1(SB) + MOVV $902, R12 + JMP callbackasm1(SB) + MOVV $903, R12 + JMP callbackasm1(SB) + MOVV $904, R12 + JMP callbackasm1(SB) + MOVV $905, R12 + JMP callbackasm1(SB) + MOVV $906, R12 + JMP callbackasm1(SB) + MOVV $907, R12 + JMP callbackasm1(SB) + MOVV $908, R12 + JMP callbackasm1(SB) + MOVV $909, R12 + JMP callbackasm1(SB) + MOVV $910, R12 + JMP callbackasm1(SB) + MOVV $911, R12 + JMP callbackasm1(SB) + MOVV $912, R12 + JMP callbackasm1(SB) + MOVV $913, R12 + JMP callbackasm1(SB) + MOVV $914, R12 + JMP callbackasm1(SB) + MOVV $915, R12 + JMP callbackasm1(SB) + MOVV $916, R12 + JMP callbackasm1(SB) + MOVV $917, R12 + JMP callbackasm1(SB) + MOVV $918, R12 + JMP callbackasm1(SB) + MOVV $919, R12 + JMP callbackasm1(SB) + MOVV $920, R12 + JMP callbackasm1(SB) + MOVV $921, R12 + JMP callbackasm1(SB) + MOVV $922, R12 + JMP callbackasm1(SB) + MOVV $923, R12 + JMP callbackasm1(SB) + MOVV $924, R12 + JMP callbackasm1(SB) + MOVV $925, R12 + JMP callbackasm1(SB) + MOVV $926, R12 + JMP callbackasm1(SB) + MOVV $927, R12 + JMP callbackasm1(SB) + MOVV $928, R12 + JMP callbackasm1(SB) + MOVV $929, R12 + JMP callbackasm1(SB) + MOVV $930, R12 + JMP callbackasm1(SB) + MOVV $931, R12 + JMP callbackasm1(SB) + MOVV $932, R12 + JMP callbackasm1(SB) + MOVV $933, R12 + JMP callbackasm1(SB) + MOVV $934, R12 + JMP callbackasm1(SB) + MOVV $935, R12 + JMP callbackasm1(SB) + MOVV $936, R12 + JMP callbackasm1(SB) + MOVV $937, R12 + JMP callbackasm1(SB) + MOVV $938, R12 + JMP callbackasm1(SB) + MOVV $939, R12 + JMP callbackasm1(SB) + MOVV $940, R12 + JMP callbackasm1(SB) + MOVV $941, R12 + JMP callbackasm1(SB) + MOVV $942, R12 + JMP callbackasm1(SB) + MOVV $943, R12 + JMP callbackasm1(SB) + MOVV $944, R12 + JMP callbackasm1(SB) + MOVV $945, R12 + JMP callbackasm1(SB) + MOVV $946, R12 + JMP callbackasm1(SB) + MOVV $947, R12 + JMP callbackasm1(SB) + MOVV $948, R12 + JMP callbackasm1(SB) + MOVV $949, R12 + JMP callbackasm1(SB) + MOVV $950, R12 + JMP callbackasm1(SB) + MOVV $951, R12 + JMP callbackasm1(SB) + MOVV $952, R12 + JMP callbackasm1(SB) + MOVV $953, R12 + JMP callbackasm1(SB) + MOVV $954, R12 + JMP callbackasm1(SB) + MOVV $955, R12 + JMP callbackasm1(SB) + MOVV $956, R12 + JMP callbackasm1(SB) + MOVV $957, R12 + JMP callbackasm1(SB) + MOVV $958, R12 + JMP callbackasm1(SB) + MOVV $959, R12 + JMP callbackasm1(SB) + MOVV $960, R12 + JMP callbackasm1(SB) + MOVV $961, R12 + JMP callbackasm1(SB) + MOVV $962, R12 + JMP callbackasm1(SB) + MOVV $963, R12 + JMP callbackasm1(SB) + MOVV $964, R12 + JMP callbackasm1(SB) + MOVV $965, R12 + JMP callbackasm1(SB) + MOVV $966, R12 + JMP callbackasm1(SB) + MOVV $967, R12 + JMP callbackasm1(SB) + MOVV $968, R12 + JMP callbackasm1(SB) + MOVV $969, R12 + JMP callbackasm1(SB) + MOVV $970, R12 + JMP callbackasm1(SB) + MOVV $971, R12 + JMP callbackasm1(SB) + MOVV $972, R12 + JMP callbackasm1(SB) + MOVV $973, R12 + JMP callbackasm1(SB) + MOVV $974, R12 + JMP callbackasm1(SB) + MOVV $975, R12 + JMP callbackasm1(SB) + MOVV $976, R12 + JMP callbackasm1(SB) + MOVV $977, R12 + JMP callbackasm1(SB) + MOVV $978, R12 + JMP callbackasm1(SB) + MOVV $979, R12 + JMP callbackasm1(SB) + MOVV $980, R12 + JMP callbackasm1(SB) + MOVV $981, R12 + JMP callbackasm1(SB) + MOVV $982, R12 + JMP callbackasm1(SB) + MOVV $983, R12 + JMP callbackasm1(SB) + MOVV $984, R12 + JMP callbackasm1(SB) + MOVV $985, R12 + JMP callbackasm1(SB) + MOVV $986, R12 + JMP callbackasm1(SB) + MOVV $987, R12 + JMP callbackasm1(SB) + MOVV $988, R12 + JMP callbackasm1(SB) + MOVV $989, R12 + JMP callbackasm1(SB) + MOVV $990, R12 + JMP callbackasm1(SB) + MOVV $991, R12 + JMP callbackasm1(SB) + MOVV $992, R12 + JMP callbackasm1(SB) + MOVV $993, R12 + JMP callbackasm1(SB) + MOVV $994, R12 + JMP callbackasm1(SB) + MOVV $995, R12 + JMP callbackasm1(SB) + MOVV $996, R12 + JMP callbackasm1(SB) + MOVV $997, R12 + JMP callbackasm1(SB) + MOVV $998, R12 + JMP callbackasm1(SB) + MOVV $999, R12 + JMP callbackasm1(SB) + MOVV $1000, R12 + JMP callbackasm1(SB) + MOVV $1001, R12 + JMP callbackasm1(SB) + MOVV $1002, R12 + JMP callbackasm1(SB) + MOVV $1003, R12 + JMP callbackasm1(SB) + MOVV $1004, R12 + JMP callbackasm1(SB) + MOVV $1005, R12 + JMP callbackasm1(SB) + MOVV $1006, R12 + JMP callbackasm1(SB) + MOVV $1007, R12 + JMP callbackasm1(SB) + MOVV $1008, R12 + JMP callbackasm1(SB) + MOVV $1009, R12 + JMP callbackasm1(SB) + MOVV $1010, R12 + JMP callbackasm1(SB) + MOVV $1011, R12 + JMP callbackasm1(SB) + MOVV $1012, R12 + JMP callbackasm1(SB) + MOVV $1013, R12 + JMP callbackasm1(SB) + MOVV $1014, R12 + JMP callbackasm1(SB) + MOVV $1015, R12 + JMP callbackasm1(SB) + MOVV $1016, R12 + JMP callbackasm1(SB) + MOVV $1017, R12 + JMP callbackasm1(SB) + MOVV $1018, R12 + JMP callbackasm1(SB) + MOVV $1019, R12 + JMP callbackasm1(SB) + MOVV $1020, R12 + JMP callbackasm1(SB) + MOVV $1021, R12 + JMP callbackasm1(SB) + MOVV $1022, R12 + JMP callbackasm1(SB) + MOVV $1023, R12 + JMP callbackasm1(SB) + MOVV $1024, R12 + JMP callbackasm1(SB) + MOVV $1025, R12 + JMP callbackasm1(SB) + MOVV $1026, R12 + JMP callbackasm1(SB) + MOVV $1027, R12 + JMP callbackasm1(SB) + MOVV $1028, R12 + JMP callbackasm1(SB) + MOVV $1029, R12 + JMP callbackasm1(SB) + MOVV $1030, R12 + JMP callbackasm1(SB) + MOVV $1031, R12 + JMP callbackasm1(SB) + MOVV $1032, R12 + JMP callbackasm1(SB) + MOVV $1033, R12 + JMP callbackasm1(SB) + MOVV $1034, R12 + JMP callbackasm1(SB) + MOVV $1035, R12 + JMP callbackasm1(SB) + MOVV $1036, R12 + JMP callbackasm1(SB) + MOVV $1037, R12 + JMP callbackasm1(SB) + MOVV $1038, R12 + JMP callbackasm1(SB) + MOVV $1039, R12 + JMP callbackasm1(SB) + MOVV $1040, R12 + JMP callbackasm1(SB) + MOVV $1041, R12 + JMP callbackasm1(SB) + MOVV $1042, R12 + JMP callbackasm1(SB) + MOVV $1043, R12 + JMP callbackasm1(SB) + MOVV $1044, R12 + JMP callbackasm1(SB) + MOVV $1045, R12 + JMP callbackasm1(SB) + MOVV $1046, R12 + JMP callbackasm1(SB) + MOVV $1047, R12 + JMP callbackasm1(SB) + MOVV $1048, R12 + JMP callbackasm1(SB) + MOVV $1049, R12 + JMP callbackasm1(SB) + MOVV $1050, R12 + JMP callbackasm1(SB) + MOVV $1051, R12 + JMP callbackasm1(SB) + MOVV $1052, R12 + JMP callbackasm1(SB) + MOVV $1053, R12 + JMP callbackasm1(SB) + MOVV $1054, R12 + JMP callbackasm1(SB) + MOVV $1055, R12 + JMP callbackasm1(SB) + MOVV $1056, R12 + JMP callbackasm1(SB) + MOVV $1057, R12 + JMP callbackasm1(SB) + MOVV $1058, R12 + JMP callbackasm1(SB) + MOVV $1059, R12 + JMP callbackasm1(SB) + MOVV $1060, R12 + JMP callbackasm1(SB) + MOVV $1061, R12 + JMP callbackasm1(SB) + MOVV $1062, R12 + JMP callbackasm1(SB) + MOVV $1063, R12 + JMP callbackasm1(SB) + MOVV $1064, R12 + JMP callbackasm1(SB) + MOVV $1065, R12 + JMP callbackasm1(SB) + MOVV $1066, R12 + JMP callbackasm1(SB) + MOVV $1067, R12 + JMP callbackasm1(SB) + MOVV $1068, R12 + JMP callbackasm1(SB) + MOVV $1069, R12 + JMP callbackasm1(SB) + MOVV $1070, R12 + JMP callbackasm1(SB) + MOVV $1071, R12 + JMP callbackasm1(SB) + MOVV $1072, R12 + JMP callbackasm1(SB) + MOVV $1073, R12 + JMP callbackasm1(SB) + MOVV $1074, R12 + JMP callbackasm1(SB) + MOVV $1075, R12 + JMP callbackasm1(SB) + MOVV $1076, R12 + JMP callbackasm1(SB) + MOVV $1077, R12 + JMP callbackasm1(SB) + MOVV $1078, R12 + JMP callbackasm1(SB) + MOVV $1079, R12 + JMP callbackasm1(SB) + MOVV $1080, R12 + JMP callbackasm1(SB) + MOVV $1081, R12 + JMP callbackasm1(SB) + MOVV $1082, R12 + JMP callbackasm1(SB) + MOVV $1083, R12 + JMP callbackasm1(SB) + MOVV $1084, R12 + JMP callbackasm1(SB) + MOVV $1085, R12 + JMP callbackasm1(SB) + MOVV $1086, R12 + JMP callbackasm1(SB) + MOVV $1087, R12 + JMP callbackasm1(SB) + MOVV $1088, R12 + JMP callbackasm1(SB) + MOVV $1089, R12 + JMP callbackasm1(SB) + MOVV $1090, R12 + JMP callbackasm1(SB) + MOVV $1091, R12 + JMP callbackasm1(SB) + MOVV $1092, R12 + JMP callbackasm1(SB) + MOVV $1093, R12 + JMP callbackasm1(SB) + MOVV $1094, R12 + JMP callbackasm1(SB) + MOVV $1095, R12 + JMP callbackasm1(SB) + MOVV $1096, R12 + JMP callbackasm1(SB) + MOVV $1097, R12 + JMP callbackasm1(SB) + MOVV $1098, R12 + JMP callbackasm1(SB) + MOVV $1099, R12 + JMP callbackasm1(SB) + MOVV $1100, R12 + JMP callbackasm1(SB) + MOVV $1101, R12 + JMP callbackasm1(SB) + MOVV $1102, R12 + JMP callbackasm1(SB) + MOVV $1103, R12 + JMP callbackasm1(SB) + MOVV $1104, R12 + JMP callbackasm1(SB) + MOVV $1105, R12 + JMP callbackasm1(SB) + MOVV $1106, R12 + JMP callbackasm1(SB) + MOVV $1107, R12 + JMP callbackasm1(SB) + MOVV $1108, R12 + JMP callbackasm1(SB) + MOVV $1109, R12 + JMP callbackasm1(SB) + MOVV $1110, R12 + JMP callbackasm1(SB) + MOVV $1111, R12 + JMP callbackasm1(SB) + MOVV $1112, R12 + JMP callbackasm1(SB) + MOVV $1113, R12 + JMP callbackasm1(SB) + MOVV $1114, R12 + JMP callbackasm1(SB) + MOVV $1115, R12 + JMP callbackasm1(SB) + MOVV $1116, R12 + JMP callbackasm1(SB) + MOVV $1117, R12 + JMP callbackasm1(SB) + MOVV $1118, R12 + JMP callbackasm1(SB) + MOVV $1119, R12 + JMP callbackasm1(SB) + MOVV $1120, R12 + JMP callbackasm1(SB) + MOVV $1121, R12 + JMP callbackasm1(SB) + MOVV $1122, R12 + JMP callbackasm1(SB) + MOVV $1123, R12 + JMP callbackasm1(SB) + MOVV $1124, R12 + JMP callbackasm1(SB) + MOVV $1125, R12 + JMP callbackasm1(SB) + MOVV $1126, R12 + JMP callbackasm1(SB) + MOVV $1127, R12 + JMP callbackasm1(SB) + MOVV $1128, R12 + JMP callbackasm1(SB) + MOVV $1129, R12 + JMP callbackasm1(SB) + MOVV $1130, R12 + JMP callbackasm1(SB) + MOVV $1131, R12 + JMP callbackasm1(SB) + MOVV $1132, R12 + JMP callbackasm1(SB) + MOVV $1133, R12 + JMP callbackasm1(SB) + MOVV $1134, R12 + JMP callbackasm1(SB) + MOVV $1135, R12 + JMP callbackasm1(SB) + MOVV $1136, R12 + JMP callbackasm1(SB) + MOVV $1137, R12 + JMP callbackasm1(SB) + MOVV $1138, R12 + JMP callbackasm1(SB) + MOVV $1139, R12 + JMP callbackasm1(SB) + MOVV $1140, R12 + JMP callbackasm1(SB) + MOVV $1141, R12 + JMP callbackasm1(SB) + MOVV $1142, R12 + JMP callbackasm1(SB) + MOVV $1143, R12 + JMP callbackasm1(SB) + MOVV $1144, R12 + JMP callbackasm1(SB) + MOVV $1145, R12 + JMP callbackasm1(SB) + MOVV $1146, R12 + JMP callbackasm1(SB) + MOVV $1147, R12 + JMP callbackasm1(SB) + MOVV $1148, R12 + JMP callbackasm1(SB) + MOVV $1149, R12 + JMP callbackasm1(SB) + MOVV $1150, R12 + JMP callbackasm1(SB) + MOVV $1151, R12 + JMP callbackasm1(SB) + MOVV $1152, R12 + JMP callbackasm1(SB) + MOVV $1153, R12 + JMP callbackasm1(SB) + MOVV $1154, R12 + JMP callbackasm1(SB) + MOVV $1155, R12 + JMP callbackasm1(SB) + MOVV $1156, R12 + JMP callbackasm1(SB) + MOVV $1157, R12 + JMP callbackasm1(SB) + MOVV $1158, R12 + JMP callbackasm1(SB) + MOVV $1159, R12 + JMP callbackasm1(SB) + MOVV $1160, R12 + JMP callbackasm1(SB) + MOVV $1161, R12 + JMP callbackasm1(SB) + MOVV $1162, R12 + JMP callbackasm1(SB) + MOVV $1163, R12 + JMP callbackasm1(SB) + MOVV $1164, R12 + JMP callbackasm1(SB) + MOVV $1165, R12 + JMP callbackasm1(SB) + MOVV $1166, R12 + JMP callbackasm1(SB) + MOVV $1167, R12 + JMP callbackasm1(SB) + MOVV $1168, R12 + JMP callbackasm1(SB) + MOVV $1169, R12 + JMP callbackasm1(SB) + MOVV $1170, R12 + JMP callbackasm1(SB) + MOVV $1171, R12 + JMP callbackasm1(SB) + MOVV $1172, R12 + JMP callbackasm1(SB) + MOVV $1173, R12 + JMP callbackasm1(SB) + MOVV $1174, R12 + JMP callbackasm1(SB) + MOVV $1175, R12 + JMP callbackasm1(SB) + MOVV $1176, R12 + JMP callbackasm1(SB) + MOVV $1177, R12 + JMP callbackasm1(SB) + MOVV $1178, R12 + JMP callbackasm1(SB) + MOVV $1179, R12 + JMP callbackasm1(SB) + MOVV $1180, R12 + JMP callbackasm1(SB) + MOVV $1181, R12 + JMP callbackasm1(SB) + MOVV $1182, R12 + JMP callbackasm1(SB) + MOVV $1183, R12 + JMP callbackasm1(SB) + MOVV $1184, R12 + JMP callbackasm1(SB) + MOVV $1185, R12 + JMP callbackasm1(SB) + MOVV $1186, R12 + JMP callbackasm1(SB) + MOVV $1187, R12 + JMP callbackasm1(SB) + MOVV $1188, R12 + JMP callbackasm1(SB) + MOVV $1189, R12 + JMP callbackasm1(SB) + MOVV $1190, R12 + JMP callbackasm1(SB) + MOVV $1191, R12 + JMP callbackasm1(SB) + MOVV $1192, R12 + JMP callbackasm1(SB) + MOVV $1193, R12 + JMP callbackasm1(SB) + MOVV $1194, R12 + JMP callbackasm1(SB) + MOVV $1195, R12 + JMP callbackasm1(SB) + MOVV $1196, R12 + JMP callbackasm1(SB) + MOVV $1197, R12 + JMP callbackasm1(SB) + MOVV $1198, R12 + JMP callbackasm1(SB) + MOVV $1199, R12 + JMP callbackasm1(SB) + MOVV $1200, R12 + JMP callbackasm1(SB) + MOVV $1201, R12 + JMP callbackasm1(SB) + MOVV $1202, R12 + JMP callbackasm1(SB) + MOVV $1203, R12 + JMP callbackasm1(SB) + MOVV $1204, R12 + JMP callbackasm1(SB) + MOVV $1205, R12 + JMP callbackasm1(SB) + MOVV $1206, R12 + JMP callbackasm1(SB) + MOVV $1207, R12 + JMP callbackasm1(SB) + MOVV $1208, R12 + JMP callbackasm1(SB) + MOVV $1209, R12 + JMP callbackasm1(SB) + MOVV $1210, R12 + JMP callbackasm1(SB) + MOVV $1211, R12 + JMP callbackasm1(SB) + MOVV $1212, R12 + JMP callbackasm1(SB) + MOVV $1213, R12 + JMP callbackasm1(SB) + MOVV $1214, R12 + JMP callbackasm1(SB) + MOVV $1215, R12 + JMP callbackasm1(SB) + MOVV $1216, R12 + JMP callbackasm1(SB) + MOVV $1217, R12 + JMP callbackasm1(SB) + MOVV $1218, R12 + JMP callbackasm1(SB) + MOVV $1219, R12 + JMP callbackasm1(SB) + MOVV $1220, R12 + JMP callbackasm1(SB) + MOVV $1221, R12 + JMP callbackasm1(SB) + MOVV $1222, R12 + JMP callbackasm1(SB) + MOVV $1223, R12 + JMP callbackasm1(SB) + MOVV $1224, R12 + JMP callbackasm1(SB) + MOVV $1225, R12 + JMP callbackasm1(SB) + MOVV $1226, R12 + JMP callbackasm1(SB) + MOVV $1227, R12 + JMP callbackasm1(SB) + MOVV $1228, R12 + JMP callbackasm1(SB) + MOVV $1229, R12 + JMP callbackasm1(SB) + MOVV $1230, R12 + JMP callbackasm1(SB) + MOVV $1231, R12 + JMP callbackasm1(SB) + MOVV $1232, R12 + JMP callbackasm1(SB) + MOVV $1233, R12 + JMP callbackasm1(SB) + MOVV $1234, R12 + JMP callbackasm1(SB) + MOVV $1235, R12 + JMP callbackasm1(SB) + MOVV $1236, R12 + JMP callbackasm1(SB) + MOVV $1237, R12 + JMP callbackasm1(SB) + MOVV $1238, R12 + JMP callbackasm1(SB) + MOVV $1239, R12 + JMP callbackasm1(SB) + MOVV $1240, R12 + JMP callbackasm1(SB) + MOVV $1241, R12 + JMP callbackasm1(SB) + MOVV $1242, R12 + JMP callbackasm1(SB) + MOVV $1243, R12 + JMP callbackasm1(SB) + MOVV $1244, R12 + JMP callbackasm1(SB) + MOVV $1245, R12 + JMP callbackasm1(SB) + MOVV $1246, R12 + JMP callbackasm1(SB) + MOVV $1247, R12 + JMP callbackasm1(SB) + MOVV $1248, R12 + JMP callbackasm1(SB) + MOVV $1249, R12 + JMP callbackasm1(SB) + MOVV $1250, R12 + JMP callbackasm1(SB) + MOVV $1251, R12 + JMP callbackasm1(SB) + MOVV $1252, R12 + JMP callbackasm1(SB) + MOVV $1253, R12 + JMP callbackasm1(SB) + MOVV $1254, R12 + JMP callbackasm1(SB) + MOVV $1255, R12 + JMP callbackasm1(SB) + MOVV $1256, R12 + JMP callbackasm1(SB) + MOVV $1257, R12 + JMP callbackasm1(SB) + MOVV $1258, R12 + JMP callbackasm1(SB) + MOVV $1259, R12 + JMP callbackasm1(SB) + MOVV $1260, R12 + JMP callbackasm1(SB) + MOVV $1261, R12 + JMP callbackasm1(SB) + MOVV $1262, R12 + JMP callbackasm1(SB) + MOVV $1263, R12 + JMP callbackasm1(SB) + MOVV $1264, R12 + JMP callbackasm1(SB) + MOVV $1265, R12 + JMP callbackasm1(SB) + MOVV $1266, R12 + JMP callbackasm1(SB) + MOVV $1267, R12 + JMP callbackasm1(SB) + MOVV $1268, R12 + JMP callbackasm1(SB) + MOVV $1269, R12 + JMP callbackasm1(SB) + MOVV $1270, R12 + JMP callbackasm1(SB) + MOVV $1271, R12 + JMP callbackasm1(SB) + MOVV $1272, R12 + JMP callbackasm1(SB) + MOVV $1273, R12 + JMP callbackasm1(SB) + MOVV $1274, R12 + JMP callbackasm1(SB) + MOVV $1275, R12 + JMP callbackasm1(SB) + MOVV $1276, R12 + JMP callbackasm1(SB) + MOVV $1277, R12 + JMP callbackasm1(SB) + MOVV $1278, R12 + JMP callbackasm1(SB) + MOVV $1279, R12 + JMP callbackasm1(SB) + MOVV $1280, R12 + JMP callbackasm1(SB) + MOVV $1281, R12 + JMP callbackasm1(SB) + MOVV $1282, R12 + JMP callbackasm1(SB) + MOVV $1283, R12 + JMP callbackasm1(SB) + MOVV $1284, R12 + JMP callbackasm1(SB) + MOVV $1285, R12 + JMP callbackasm1(SB) + MOVV $1286, R12 + JMP callbackasm1(SB) + MOVV $1287, R12 + JMP callbackasm1(SB) + MOVV $1288, R12 + JMP callbackasm1(SB) + MOVV $1289, R12 + JMP callbackasm1(SB) + MOVV $1290, R12 + JMP callbackasm1(SB) + MOVV $1291, R12 + JMP callbackasm1(SB) + MOVV $1292, R12 + JMP callbackasm1(SB) + MOVV $1293, R12 + JMP callbackasm1(SB) + MOVV $1294, R12 + JMP callbackasm1(SB) + MOVV $1295, R12 + JMP callbackasm1(SB) + MOVV $1296, R12 + JMP callbackasm1(SB) + MOVV $1297, R12 + JMP callbackasm1(SB) + MOVV $1298, R12 + JMP callbackasm1(SB) + MOVV $1299, R12 + JMP callbackasm1(SB) + MOVV $1300, R12 + JMP callbackasm1(SB) + MOVV $1301, R12 + JMP callbackasm1(SB) + MOVV $1302, R12 + JMP callbackasm1(SB) + MOVV $1303, R12 + JMP callbackasm1(SB) + MOVV $1304, R12 + JMP callbackasm1(SB) + MOVV $1305, R12 + JMP callbackasm1(SB) + MOVV $1306, R12 + JMP callbackasm1(SB) + MOVV $1307, R12 + JMP callbackasm1(SB) + MOVV $1308, R12 + JMP callbackasm1(SB) + MOVV $1309, R12 + JMP callbackasm1(SB) + MOVV $1310, R12 + JMP callbackasm1(SB) + MOVV $1311, R12 + JMP callbackasm1(SB) + MOVV $1312, R12 + JMP callbackasm1(SB) + MOVV $1313, R12 + JMP callbackasm1(SB) + MOVV $1314, R12 + JMP callbackasm1(SB) + MOVV $1315, R12 + JMP callbackasm1(SB) + MOVV $1316, R12 + JMP callbackasm1(SB) + MOVV $1317, R12 + JMP callbackasm1(SB) + MOVV $1318, R12 + JMP callbackasm1(SB) + MOVV $1319, R12 + JMP callbackasm1(SB) + MOVV $1320, R12 + JMP callbackasm1(SB) + MOVV $1321, R12 + JMP callbackasm1(SB) + MOVV $1322, R12 + JMP callbackasm1(SB) + MOVV $1323, R12 + JMP callbackasm1(SB) + MOVV $1324, R12 + JMP callbackasm1(SB) + MOVV $1325, R12 + JMP callbackasm1(SB) + MOVV $1326, R12 + JMP callbackasm1(SB) + MOVV $1327, R12 + JMP callbackasm1(SB) + MOVV $1328, R12 + JMP callbackasm1(SB) + MOVV $1329, R12 + JMP callbackasm1(SB) + MOVV $1330, R12 + JMP callbackasm1(SB) + MOVV $1331, R12 + JMP callbackasm1(SB) + MOVV $1332, R12 + JMP callbackasm1(SB) + MOVV $1333, R12 + JMP callbackasm1(SB) + MOVV $1334, R12 + JMP callbackasm1(SB) + MOVV $1335, R12 + JMP callbackasm1(SB) + MOVV $1336, R12 + JMP callbackasm1(SB) + MOVV $1337, R12 + JMP callbackasm1(SB) + MOVV $1338, R12 + JMP callbackasm1(SB) + MOVV $1339, R12 + JMP callbackasm1(SB) + MOVV $1340, R12 + JMP callbackasm1(SB) + MOVV $1341, R12 + JMP callbackasm1(SB) + MOVV $1342, R12 + JMP callbackasm1(SB) + MOVV $1343, R12 + JMP callbackasm1(SB) + MOVV $1344, R12 + JMP callbackasm1(SB) + MOVV $1345, R12 + JMP callbackasm1(SB) + MOVV $1346, R12 + JMP callbackasm1(SB) + MOVV $1347, R12 + JMP callbackasm1(SB) + MOVV $1348, R12 + JMP callbackasm1(SB) + MOVV $1349, R12 + JMP callbackasm1(SB) + MOVV $1350, R12 + JMP callbackasm1(SB) + MOVV $1351, R12 + JMP callbackasm1(SB) + MOVV $1352, R12 + JMP callbackasm1(SB) + MOVV $1353, R12 + JMP callbackasm1(SB) + MOVV $1354, R12 + JMP callbackasm1(SB) + MOVV $1355, R12 + JMP callbackasm1(SB) + MOVV $1356, R12 + JMP callbackasm1(SB) + MOVV $1357, R12 + JMP callbackasm1(SB) + MOVV $1358, R12 + JMP callbackasm1(SB) + MOVV $1359, R12 + JMP callbackasm1(SB) + MOVV $1360, R12 + JMP callbackasm1(SB) + MOVV $1361, R12 + JMP callbackasm1(SB) + MOVV $1362, R12 + JMP callbackasm1(SB) + MOVV $1363, R12 + JMP callbackasm1(SB) + MOVV $1364, R12 + JMP callbackasm1(SB) + MOVV $1365, R12 + JMP callbackasm1(SB) + MOVV $1366, R12 + JMP callbackasm1(SB) + MOVV $1367, R12 + JMP callbackasm1(SB) + MOVV $1368, R12 + JMP callbackasm1(SB) + MOVV $1369, R12 + JMP callbackasm1(SB) + MOVV $1370, R12 + JMP callbackasm1(SB) + MOVV $1371, R12 + JMP callbackasm1(SB) + MOVV $1372, R12 + JMP callbackasm1(SB) + MOVV $1373, R12 + JMP callbackasm1(SB) + MOVV $1374, R12 + JMP callbackasm1(SB) + MOVV $1375, R12 + JMP callbackasm1(SB) + MOVV $1376, R12 + JMP callbackasm1(SB) + MOVV $1377, R12 + JMP callbackasm1(SB) + MOVV $1378, R12 + JMP callbackasm1(SB) + MOVV $1379, R12 + JMP callbackasm1(SB) + MOVV $1380, R12 + JMP callbackasm1(SB) + MOVV $1381, R12 + JMP callbackasm1(SB) + MOVV $1382, R12 + JMP callbackasm1(SB) + MOVV $1383, R12 + JMP callbackasm1(SB) + MOVV $1384, R12 + JMP callbackasm1(SB) + MOVV $1385, R12 + JMP callbackasm1(SB) + MOVV $1386, R12 + JMP callbackasm1(SB) + MOVV $1387, R12 + JMP callbackasm1(SB) + MOVV $1388, R12 + JMP callbackasm1(SB) + MOVV $1389, R12 + JMP callbackasm1(SB) + MOVV $1390, R12 + JMP callbackasm1(SB) + MOVV $1391, R12 + JMP callbackasm1(SB) + MOVV $1392, R12 + JMP callbackasm1(SB) + MOVV $1393, R12 + JMP callbackasm1(SB) + MOVV $1394, R12 + JMP callbackasm1(SB) + MOVV $1395, R12 + JMP callbackasm1(SB) + MOVV $1396, R12 + JMP callbackasm1(SB) + MOVV $1397, R12 + JMP callbackasm1(SB) + MOVV $1398, R12 + JMP callbackasm1(SB) + MOVV $1399, R12 + JMP callbackasm1(SB) + MOVV $1400, R12 + JMP callbackasm1(SB) + MOVV $1401, R12 + JMP callbackasm1(SB) + MOVV $1402, R12 + JMP callbackasm1(SB) + MOVV $1403, R12 + JMP callbackasm1(SB) + MOVV $1404, R12 + JMP callbackasm1(SB) + MOVV $1405, R12 + JMP callbackasm1(SB) + MOVV $1406, R12 + JMP callbackasm1(SB) + MOVV $1407, R12 + JMP callbackasm1(SB) + MOVV $1408, R12 + JMP callbackasm1(SB) + MOVV $1409, R12 + JMP callbackasm1(SB) + MOVV $1410, R12 + JMP callbackasm1(SB) + MOVV $1411, R12 + JMP callbackasm1(SB) + MOVV $1412, R12 + JMP callbackasm1(SB) + MOVV $1413, R12 + JMP callbackasm1(SB) + MOVV $1414, R12 + JMP callbackasm1(SB) + MOVV $1415, R12 + JMP callbackasm1(SB) + MOVV $1416, R12 + JMP callbackasm1(SB) + MOVV $1417, R12 + JMP callbackasm1(SB) + MOVV $1418, R12 + JMP callbackasm1(SB) + MOVV $1419, R12 + JMP callbackasm1(SB) + MOVV $1420, R12 + JMP callbackasm1(SB) + MOVV $1421, R12 + JMP callbackasm1(SB) + MOVV $1422, R12 + JMP callbackasm1(SB) + MOVV $1423, R12 + JMP callbackasm1(SB) + MOVV $1424, R12 + JMP callbackasm1(SB) + MOVV $1425, R12 + JMP callbackasm1(SB) + MOVV $1426, R12 + JMP callbackasm1(SB) + MOVV $1427, R12 + JMP callbackasm1(SB) + MOVV $1428, R12 + JMP callbackasm1(SB) + MOVV $1429, R12 + JMP callbackasm1(SB) + MOVV $1430, R12 + JMP callbackasm1(SB) + MOVV $1431, R12 + JMP callbackasm1(SB) + MOVV $1432, R12 + JMP callbackasm1(SB) + MOVV $1433, R12 + JMP callbackasm1(SB) + MOVV $1434, R12 + JMP callbackasm1(SB) + MOVV $1435, R12 + JMP callbackasm1(SB) + MOVV $1436, R12 + JMP callbackasm1(SB) + MOVV $1437, R12 + JMP callbackasm1(SB) + MOVV $1438, R12 + JMP callbackasm1(SB) + MOVV $1439, R12 + JMP callbackasm1(SB) + MOVV $1440, R12 + JMP callbackasm1(SB) + MOVV $1441, R12 + JMP callbackasm1(SB) + MOVV $1442, R12 + JMP callbackasm1(SB) + MOVV $1443, R12 + JMP callbackasm1(SB) + MOVV $1444, R12 + JMP callbackasm1(SB) + MOVV $1445, R12 + JMP callbackasm1(SB) + MOVV $1446, R12 + JMP callbackasm1(SB) + MOVV $1447, R12 + JMP callbackasm1(SB) + MOVV $1448, R12 + JMP callbackasm1(SB) + MOVV $1449, R12 + JMP callbackasm1(SB) + MOVV $1450, R12 + JMP callbackasm1(SB) + MOVV $1451, R12 + JMP callbackasm1(SB) + MOVV $1452, R12 + JMP callbackasm1(SB) + MOVV $1453, R12 + JMP callbackasm1(SB) + MOVV $1454, R12 + JMP callbackasm1(SB) + MOVV $1455, R12 + JMP callbackasm1(SB) + MOVV $1456, R12 + JMP callbackasm1(SB) + MOVV $1457, R12 + JMP callbackasm1(SB) + MOVV $1458, R12 + JMP callbackasm1(SB) + MOVV $1459, R12 + JMP callbackasm1(SB) + MOVV $1460, R12 + JMP callbackasm1(SB) + MOVV $1461, R12 + JMP callbackasm1(SB) + MOVV $1462, R12 + JMP callbackasm1(SB) + MOVV $1463, R12 + JMP callbackasm1(SB) + MOVV $1464, R12 + JMP callbackasm1(SB) + MOVV $1465, R12 + JMP callbackasm1(SB) + MOVV $1466, R12 + JMP callbackasm1(SB) + MOVV $1467, R12 + JMP callbackasm1(SB) + MOVV $1468, R12 + JMP callbackasm1(SB) + MOVV $1469, R12 + JMP callbackasm1(SB) + MOVV $1470, R12 + JMP callbackasm1(SB) + MOVV $1471, R12 + JMP callbackasm1(SB) + MOVV $1472, R12 + JMP callbackasm1(SB) + MOVV $1473, R12 + JMP callbackasm1(SB) + MOVV $1474, R12 + JMP callbackasm1(SB) + MOVV $1475, R12 + JMP callbackasm1(SB) + MOVV $1476, R12 + JMP callbackasm1(SB) + MOVV $1477, R12 + JMP callbackasm1(SB) + MOVV $1478, R12 + JMP callbackasm1(SB) + MOVV $1479, R12 + JMP callbackasm1(SB) + MOVV $1480, R12 + JMP callbackasm1(SB) + MOVV $1481, R12 + JMP callbackasm1(SB) + MOVV $1482, R12 + JMP callbackasm1(SB) + MOVV $1483, R12 + JMP callbackasm1(SB) + MOVV $1484, R12 + JMP callbackasm1(SB) + MOVV $1485, R12 + JMP callbackasm1(SB) + MOVV $1486, R12 + JMP callbackasm1(SB) + MOVV $1487, R12 + JMP callbackasm1(SB) + MOVV $1488, R12 + JMP callbackasm1(SB) + MOVV $1489, R12 + JMP callbackasm1(SB) + MOVV $1490, R12 + JMP callbackasm1(SB) + MOVV $1491, R12 + JMP callbackasm1(SB) + MOVV $1492, R12 + JMP callbackasm1(SB) + MOVV $1493, R12 + JMP callbackasm1(SB) + MOVV $1494, R12 + JMP callbackasm1(SB) + MOVV $1495, R12 + JMP callbackasm1(SB) + MOVV $1496, R12 + JMP callbackasm1(SB) + MOVV $1497, R12 + JMP callbackasm1(SB) + MOVV $1498, R12 + JMP callbackasm1(SB) + MOVV $1499, R12 + JMP callbackasm1(SB) + MOVV $1500, R12 + JMP callbackasm1(SB) + MOVV $1501, R12 + JMP callbackasm1(SB) + MOVV $1502, R12 + JMP callbackasm1(SB) + MOVV $1503, R12 + JMP callbackasm1(SB) + MOVV $1504, R12 + JMP callbackasm1(SB) + MOVV $1505, R12 + JMP callbackasm1(SB) + MOVV $1506, R12 + JMP callbackasm1(SB) + MOVV $1507, R12 + JMP callbackasm1(SB) + MOVV $1508, R12 + JMP callbackasm1(SB) + MOVV $1509, R12 + JMP callbackasm1(SB) + MOVV $1510, R12 + JMP callbackasm1(SB) + MOVV $1511, R12 + JMP callbackasm1(SB) + MOVV $1512, R12 + JMP callbackasm1(SB) + MOVV $1513, R12 + JMP callbackasm1(SB) + MOVV $1514, R12 + JMP callbackasm1(SB) + MOVV $1515, R12 + JMP callbackasm1(SB) + MOVV $1516, R12 + JMP callbackasm1(SB) + MOVV $1517, R12 + JMP callbackasm1(SB) + MOVV $1518, R12 + JMP callbackasm1(SB) + MOVV $1519, R12 + JMP callbackasm1(SB) + MOVV $1520, R12 + JMP callbackasm1(SB) + MOVV $1521, R12 + JMP callbackasm1(SB) + MOVV $1522, R12 + JMP callbackasm1(SB) + MOVV $1523, R12 + JMP callbackasm1(SB) + MOVV $1524, R12 + JMP callbackasm1(SB) + MOVV $1525, R12 + JMP callbackasm1(SB) + MOVV $1526, R12 + JMP callbackasm1(SB) + MOVV $1527, R12 + JMP callbackasm1(SB) + MOVV $1528, R12 + JMP callbackasm1(SB) + MOVV $1529, R12 + JMP callbackasm1(SB) + MOVV $1530, R12 + JMP callbackasm1(SB) + MOVV $1531, R12 + JMP callbackasm1(SB) + MOVV $1532, R12 + JMP callbackasm1(SB) + MOVV $1533, R12 + JMP callbackasm1(SB) + MOVV $1534, R12 + JMP callbackasm1(SB) + MOVV $1535, R12 + JMP callbackasm1(SB) + MOVV $1536, R12 + JMP callbackasm1(SB) + MOVV $1537, R12 + JMP callbackasm1(SB) + MOVV $1538, R12 + JMP callbackasm1(SB) + MOVV $1539, R12 + JMP callbackasm1(SB) + MOVV $1540, R12 + JMP callbackasm1(SB) + MOVV $1541, R12 + JMP callbackasm1(SB) + MOVV $1542, R12 + JMP callbackasm1(SB) + MOVV $1543, R12 + JMP callbackasm1(SB) + MOVV $1544, R12 + JMP callbackasm1(SB) + MOVV $1545, R12 + JMP callbackasm1(SB) + MOVV $1546, R12 + JMP callbackasm1(SB) + MOVV $1547, R12 + JMP callbackasm1(SB) + MOVV $1548, R12 + JMP callbackasm1(SB) + MOVV $1549, R12 + JMP callbackasm1(SB) + MOVV $1550, R12 + JMP callbackasm1(SB) + MOVV $1551, R12 + JMP callbackasm1(SB) + MOVV $1552, R12 + JMP callbackasm1(SB) + MOVV $1553, R12 + JMP callbackasm1(SB) + MOVV $1554, R12 + JMP callbackasm1(SB) + MOVV $1555, R12 + JMP callbackasm1(SB) + MOVV $1556, R12 + JMP callbackasm1(SB) + MOVV $1557, R12 + JMP callbackasm1(SB) + MOVV $1558, R12 + JMP callbackasm1(SB) + MOVV $1559, R12 + JMP callbackasm1(SB) + MOVV $1560, R12 + JMP callbackasm1(SB) + MOVV $1561, R12 + JMP callbackasm1(SB) + MOVV $1562, R12 + JMP callbackasm1(SB) + MOVV $1563, R12 + JMP callbackasm1(SB) + MOVV $1564, R12 + JMP callbackasm1(SB) + MOVV $1565, R12 + JMP callbackasm1(SB) + MOVV $1566, R12 + JMP callbackasm1(SB) + MOVV $1567, R12 + JMP callbackasm1(SB) + MOVV $1568, R12 + JMP callbackasm1(SB) + MOVV $1569, R12 + JMP callbackasm1(SB) + MOVV $1570, R12 + JMP callbackasm1(SB) + MOVV $1571, R12 + JMP callbackasm1(SB) + MOVV $1572, R12 + JMP callbackasm1(SB) + MOVV $1573, R12 + JMP callbackasm1(SB) + MOVV $1574, R12 + JMP callbackasm1(SB) + MOVV $1575, R12 + JMP callbackasm1(SB) + MOVV $1576, R12 + JMP callbackasm1(SB) + MOVV $1577, R12 + JMP callbackasm1(SB) + MOVV $1578, R12 + JMP callbackasm1(SB) + MOVV $1579, R12 + JMP callbackasm1(SB) + MOVV $1580, R12 + JMP callbackasm1(SB) + MOVV $1581, R12 + JMP callbackasm1(SB) + MOVV $1582, R12 + JMP callbackasm1(SB) + MOVV $1583, R12 + JMP callbackasm1(SB) + MOVV $1584, R12 + JMP callbackasm1(SB) + MOVV $1585, R12 + JMP callbackasm1(SB) + MOVV $1586, R12 + JMP callbackasm1(SB) + MOVV $1587, R12 + JMP callbackasm1(SB) + MOVV $1588, R12 + JMP callbackasm1(SB) + MOVV $1589, R12 + JMP callbackasm1(SB) + MOVV $1590, R12 + JMP callbackasm1(SB) + MOVV $1591, R12 + JMP callbackasm1(SB) + MOVV $1592, R12 + JMP callbackasm1(SB) + MOVV $1593, R12 + JMP callbackasm1(SB) + MOVV $1594, R12 + JMP callbackasm1(SB) + MOVV $1595, R12 + JMP callbackasm1(SB) + MOVV $1596, R12 + JMP callbackasm1(SB) + MOVV $1597, R12 + JMP callbackasm1(SB) + MOVV $1598, R12 + JMP callbackasm1(SB) + MOVV $1599, R12 + JMP callbackasm1(SB) + MOVV $1600, R12 + JMP callbackasm1(SB) + MOVV $1601, R12 + JMP callbackasm1(SB) + MOVV $1602, R12 + JMP callbackasm1(SB) + MOVV $1603, R12 + JMP callbackasm1(SB) + MOVV $1604, R12 + JMP callbackasm1(SB) + MOVV $1605, R12 + JMP callbackasm1(SB) + MOVV $1606, R12 + JMP callbackasm1(SB) + MOVV $1607, R12 + JMP callbackasm1(SB) + MOVV $1608, R12 + JMP callbackasm1(SB) + MOVV $1609, R12 + JMP callbackasm1(SB) + MOVV $1610, R12 + JMP callbackasm1(SB) + MOVV $1611, R12 + JMP callbackasm1(SB) + MOVV $1612, R12 + JMP callbackasm1(SB) + MOVV $1613, R12 + JMP callbackasm1(SB) + MOVV $1614, R12 + JMP callbackasm1(SB) + MOVV $1615, R12 + JMP callbackasm1(SB) + MOVV $1616, R12 + JMP callbackasm1(SB) + MOVV $1617, R12 + JMP callbackasm1(SB) + MOVV $1618, R12 + JMP callbackasm1(SB) + MOVV $1619, R12 + JMP callbackasm1(SB) + MOVV $1620, R12 + JMP callbackasm1(SB) + MOVV $1621, R12 + JMP callbackasm1(SB) + MOVV $1622, R12 + JMP callbackasm1(SB) + MOVV $1623, R12 + JMP callbackasm1(SB) + MOVV $1624, R12 + JMP callbackasm1(SB) + MOVV $1625, R12 + JMP callbackasm1(SB) + MOVV $1626, R12 + JMP callbackasm1(SB) + MOVV $1627, R12 + JMP callbackasm1(SB) + MOVV $1628, R12 + JMP callbackasm1(SB) + MOVV $1629, R12 + JMP callbackasm1(SB) + MOVV $1630, R12 + JMP callbackasm1(SB) + MOVV $1631, R12 + JMP callbackasm1(SB) + MOVV $1632, R12 + JMP callbackasm1(SB) + MOVV $1633, R12 + JMP callbackasm1(SB) + MOVV $1634, R12 + JMP callbackasm1(SB) + MOVV $1635, R12 + JMP callbackasm1(SB) + MOVV $1636, R12 + JMP callbackasm1(SB) + MOVV $1637, R12 + JMP callbackasm1(SB) + MOVV $1638, R12 + JMP callbackasm1(SB) + MOVV $1639, R12 + JMP callbackasm1(SB) + MOVV $1640, R12 + JMP callbackasm1(SB) + MOVV $1641, R12 + JMP callbackasm1(SB) + MOVV $1642, R12 + JMP callbackasm1(SB) + MOVV $1643, R12 + JMP callbackasm1(SB) + MOVV $1644, R12 + JMP callbackasm1(SB) + MOVV $1645, R12 + JMP callbackasm1(SB) + MOVV $1646, R12 + JMP callbackasm1(SB) + MOVV $1647, R12 + JMP callbackasm1(SB) + MOVV $1648, R12 + JMP callbackasm1(SB) + MOVV $1649, R12 + JMP callbackasm1(SB) + MOVV $1650, R12 + JMP callbackasm1(SB) + MOVV $1651, R12 + JMP callbackasm1(SB) + MOVV $1652, R12 + JMP callbackasm1(SB) + MOVV $1653, R12 + JMP callbackasm1(SB) + MOVV $1654, R12 + JMP callbackasm1(SB) + MOVV $1655, R12 + JMP callbackasm1(SB) + MOVV $1656, R12 + JMP callbackasm1(SB) + MOVV $1657, R12 + JMP callbackasm1(SB) + MOVV $1658, R12 + JMP callbackasm1(SB) + MOVV $1659, R12 + JMP callbackasm1(SB) + MOVV $1660, R12 + JMP callbackasm1(SB) + MOVV $1661, R12 + JMP callbackasm1(SB) + MOVV $1662, R12 + JMP callbackasm1(SB) + MOVV $1663, R12 + JMP callbackasm1(SB) + MOVV $1664, R12 + JMP callbackasm1(SB) + MOVV $1665, R12 + JMP callbackasm1(SB) + MOVV $1666, R12 + JMP callbackasm1(SB) + MOVV $1667, R12 + JMP callbackasm1(SB) + MOVV $1668, R12 + JMP callbackasm1(SB) + MOVV $1669, R12 + JMP callbackasm1(SB) + MOVV $1670, R12 + JMP callbackasm1(SB) + MOVV $1671, R12 + JMP callbackasm1(SB) + MOVV $1672, R12 + JMP callbackasm1(SB) + MOVV $1673, R12 + JMP callbackasm1(SB) + MOVV $1674, R12 + JMP callbackasm1(SB) + MOVV $1675, R12 + JMP callbackasm1(SB) + MOVV $1676, R12 + JMP callbackasm1(SB) + MOVV $1677, R12 + JMP callbackasm1(SB) + MOVV $1678, R12 + JMP callbackasm1(SB) + MOVV $1679, R12 + JMP callbackasm1(SB) + MOVV $1680, R12 + JMP callbackasm1(SB) + MOVV $1681, R12 + JMP callbackasm1(SB) + MOVV $1682, R12 + JMP callbackasm1(SB) + MOVV $1683, R12 + JMP callbackasm1(SB) + MOVV $1684, R12 + JMP callbackasm1(SB) + MOVV $1685, R12 + JMP callbackasm1(SB) + MOVV $1686, R12 + JMP callbackasm1(SB) + MOVV $1687, R12 + JMP callbackasm1(SB) + MOVV $1688, R12 + JMP callbackasm1(SB) + MOVV $1689, R12 + JMP callbackasm1(SB) + MOVV $1690, R12 + JMP callbackasm1(SB) + MOVV $1691, R12 + JMP callbackasm1(SB) + MOVV $1692, R12 + JMP callbackasm1(SB) + MOVV $1693, R12 + JMP callbackasm1(SB) + MOVV $1694, R12 + JMP callbackasm1(SB) + MOVV $1695, R12 + JMP callbackasm1(SB) + MOVV $1696, R12 + JMP callbackasm1(SB) + MOVV $1697, R12 + JMP callbackasm1(SB) + MOVV $1698, R12 + JMP callbackasm1(SB) + MOVV $1699, R12 + JMP callbackasm1(SB) + MOVV $1700, R12 + JMP callbackasm1(SB) + MOVV $1701, R12 + JMP callbackasm1(SB) + MOVV $1702, R12 + JMP callbackasm1(SB) + MOVV $1703, R12 + JMP callbackasm1(SB) + MOVV $1704, R12 + JMP callbackasm1(SB) + MOVV $1705, R12 + JMP callbackasm1(SB) + MOVV $1706, R12 + JMP callbackasm1(SB) + MOVV $1707, R12 + JMP callbackasm1(SB) + MOVV $1708, R12 + JMP callbackasm1(SB) + MOVV $1709, R12 + JMP callbackasm1(SB) + MOVV $1710, R12 + JMP callbackasm1(SB) + MOVV $1711, R12 + JMP callbackasm1(SB) + MOVV $1712, R12 + JMP callbackasm1(SB) + MOVV $1713, R12 + JMP callbackasm1(SB) + MOVV $1714, R12 + JMP callbackasm1(SB) + MOVV $1715, R12 + JMP callbackasm1(SB) + MOVV $1716, R12 + JMP callbackasm1(SB) + MOVV $1717, R12 + JMP callbackasm1(SB) + MOVV $1718, R12 + JMP callbackasm1(SB) + MOVV $1719, R12 + JMP callbackasm1(SB) + MOVV $1720, R12 + JMP callbackasm1(SB) + MOVV $1721, R12 + JMP callbackasm1(SB) + MOVV $1722, R12 + JMP callbackasm1(SB) + MOVV $1723, R12 + JMP callbackasm1(SB) + MOVV $1724, R12 + JMP callbackasm1(SB) + MOVV $1725, R12 + JMP callbackasm1(SB) + MOVV $1726, R12 + JMP callbackasm1(SB) + MOVV $1727, R12 + JMP callbackasm1(SB) + MOVV $1728, R12 + JMP callbackasm1(SB) + MOVV $1729, R12 + JMP callbackasm1(SB) + MOVV $1730, R12 + JMP callbackasm1(SB) + MOVV $1731, R12 + JMP callbackasm1(SB) + MOVV $1732, R12 + JMP callbackasm1(SB) + MOVV $1733, R12 + JMP callbackasm1(SB) + MOVV $1734, R12 + JMP callbackasm1(SB) + MOVV $1735, R12 + JMP callbackasm1(SB) + MOVV $1736, R12 + JMP callbackasm1(SB) + MOVV $1737, R12 + JMP callbackasm1(SB) + MOVV $1738, R12 + JMP callbackasm1(SB) + MOVV $1739, R12 + JMP callbackasm1(SB) + MOVV $1740, R12 + JMP callbackasm1(SB) + MOVV $1741, R12 + JMP callbackasm1(SB) + MOVV $1742, R12 + JMP callbackasm1(SB) + MOVV $1743, R12 + JMP callbackasm1(SB) + MOVV $1744, R12 + JMP callbackasm1(SB) + MOVV $1745, R12 + JMP callbackasm1(SB) + MOVV $1746, R12 + JMP callbackasm1(SB) + MOVV $1747, R12 + JMP callbackasm1(SB) + MOVV $1748, R12 + JMP callbackasm1(SB) + MOVV $1749, R12 + JMP callbackasm1(SB) + MOVV $1750, R12 + JMP callbackasm1(SB) + MOVV $1751, R12 + JMP callbackasm1(SB) + MOVV $1752, R12 + JMP callbackasm1(SB) + MOVV $1753, R12 + JMP callbackasm1(SB) + MOVV $1754, R12 + JMP callbackasm1(SB) + MOVV $1755, R12 + JMP callbackasm1(SB) + MOVV $1756, R12 + JMP callbackasm1(SB) + MOVV $1757, R12 + JMP callbackasm1(SB) + MOVV $1758, R12 + JMP callbackasm1(SB) + MOVV $1759, R12 + JMP callbackasm1(SB) + MOVV $1760, R12 + JMP callbackasm1(SB) + MOVV $1761, R12 + JMP callbackasm1(SB) + MOVV $1762, R12 + JMP callbackasm1(SB) + MOVV $1763, R12 + JMP callbackasm1(SB) + MOVV $1764, R12 + JMP callbackasm1(SB) + MOVV $1765, R12 + JMP callbackasm1(SB) + MOVV $1766, R12 + JMP callbackasm1(SB) + MOVV $1767, R12 + JMP callbackasm1(SB) + MOVV $1768, R12 + JMP callbackasm1(SB) + MOVV $1769, R12 + JMP callbackasm1(SB) + MOVV $1770, R12 + JMP callbackasm1(SB) + MOVV $1771, R12 + JMP callbackasm1(SB) + MOVV $1772, R12 + JMP callbackasm1(SB) + MOVV $1773, R12 + JMP callbackasm1(SB) + MOVV $1774, R12 + JMP callbackasm1(SB) + MOVV $1775, R12 + JMP callbackasm1(SB) + MOVV $1776, R12 + JMP callbackasm1(SB) + MOVV $1777, R12 + JMP callbackasm1(SB) + MOVV $1778, R12 + JMP callbackasm1(SB) + MOVV $1779, R12 + JMP callbackasm1(SB) + MOVV $1780, R12 + JMP callbackasm1(SB) + MOVV $1781, R12 + JMP callbackasm1(SB) + MOVV $1782, R12 + JMP callbackasm1(SB) + MOVV $1783, R12 + JMP callbackasm1(SB) + MOVV $1784, R12 + JMP callbackasm1(SB) + MOVV $1785, R12 + JMP callbackasm1(SB) + MOVV $1786, R12 + JMP callbackasm1(SB) + MOVV $1787, R12 + JMP callbackasm1(SB) + MOVV $1788, R12 + JMP callbackasm1(SB) + MOVV $1789, R12 + JMP callbackasm1(SB) + MOVV $1790, R12 + JMP callbackasm1(SB) + MOVV $1791, R12 + JMP callbackasm1(SB) + MOVV $1792, R12 + JMP callbackasm1(SB) + MOVV $1793, R12 + JMP callbackasm1(SB) + MOVV $1794, R12 + JMP callbackasm1(SB) + MOVV $1795, R12 + JMP callbackasm1(SB) + MOVV $1796, R12 + JMP callbackasm1(SB) + MOVV $1797, R12 + JMP callbackasm1(SB) + MOVV $1798, R12 + JMP callbackasm1(SB) + MOVV $1799, R12 + JMP callbackasm1(SB) + MOVV $1800, R12 + JMP callbackasm1(SB) + MOVV $1801, R12 + JMP callbackasm1(SB) + MOVV $1802, R12 + JMP callbackasm1(SB) + MOVV $1803, R12 + JMP callbackasm1(SB) + MOVV $1804, R12 + JMP callbackasm1(SB) + MOVV $1805, R12 + JMP callbackasm1(SB) + MOVV $1806, R12 + JMP callbackasm1(SB) + MOVV $1807, R12 + JMP callbackasm1(SB) + MOVV $1808, R12 + JMP callbackasm1(SB) + MOVV $1809, R12 + JMP callbackasm1(SB) + MOVV $1810, R12 + JMP callbackasm1(SB) + MOVV $1811, R12 + JMP callbackasm1(SB) + MOVV $1812, R12 + JMP callbackasm1(SB) + MOVV $1813, R12 + JMP callbackasm1(SB) + MOVV $1814, R12 + JMP callbackasm1(SB) + MOVV $1815, R12 + JMP callbackasm1(SB) + MOVV $1816, R12 + JMP callbackasm1(SB) + MOVV $1817, R12 + JMP callbackasm1(SB) + MOVV $1818, R12 + JMP callbackasm1(SB) + MOVV $1819, R12 + JMP callbackasm1(SB) + MOVV $1820, R12 + JMP callbackasm1(SB) + MOVV $1821, R12 + JMP callbackasm1(SB) + MOVV $1822, R12 + JMP callbackasm1(SB) + MOVV $1823, R12 + JMP callbackasm1(SB) + MOVV $1824, R12 + JMP callbackasm1(SB) + MOVV $1825, R12 + JMP callbackasm1(SB) + MOVV $1826, R12 + JMP callbackasm1(SB) + MOVV $1827, R12 + JMP callbackasm1(SB) + MOVV $1828, R12 + JMP callbackasm1(SB) + MOVV $1829, R12 + JMP callbackasm1(SB) + MOVV $1830, R12 + JMP callbackasm1(SB) + MOVV $1831, R12 + JMP callbackasm1(SB) + MOVV $1832, R12 + JMP callbackasm1(SB) + MOVV $1833, R12 + JMP callbackasm1(SB) + MOVV $1834, R12 + JMP callbackasm1(SB) + MOVV $1835, R12 + JMP callbackasm1(SB) + MOVV $1836, R12 + JMP callbackasm1(SB) + MOVV $1837, R12 + JMP callbackasm1(SB) + MOVV $1838, R12 + JMP callbackasm1(SB) + MOVV $1839, R12 + JMP callbackasm1(SB) + MOVV $1840, R12 + JMP callbackasm1(SB) + MOVV $1841, R12 + JMP callbackasm1(SB) + MOVV $1842, R12 + JMP callbackasm1(SB) + MOVV $1843, R12 + JMP callbackasm1(SB) + MOVV $1844, R12 + JMP callbackasm1(SB) + MOVV $1845, R12 + JMP callbackasm1(SB) + MOVV $1846, R12 + JMP callbackasm1(SB) + MOVV $1847, R12 + JMP callbackasm1(SB) + MOVV $1848, R12 + JMP callbackasm1(SB) + MOVV $1849, R12 + JMP callbackasm1(SB) + MOVV $1850, R12 + JMP callbackasm1(SB) + MOVV $1851, R12 + JMP callbackasm1(SB) + MOVV $1852, R12 + JMP callbackasm1(SB) + MOVV $1853, R12 + JMP callbackasm1(SB) + MOVV $1854, R12 + JMP callbackasm1(SB) + MOVV $1855, R12 + JMP callbackasm1(SB) + MOVV $1856, R12 + JMP callbackasm1(SB) + MOVV $1857, R12 + JMP callbackasm1(SB) + MOVV $1858, R12 + JMP callbackasm1(SB) + MOVV $1859, R12 + JMP callbackasm1(SB) + MOVV $1860, R12 + JMP callbackasm1(SB) + MOVV $1861, R12 + JMP callbackasm1(SB) + MOVV $1862, R12 + JMP callbackasm1(SB) + MOVV $1863, R12 + JMP callbackasm1(SB) + MOVV $1864, R12 + JMP callbackasm1(SB) + MOVV $1865, R12 + JMP callbackasm1(SB) + MOVV $1866, R12 + JMP callbackasm1(SB) + MOVV $1867, R12 + JMP callbackasm1(SB) + MOVV $1868, R12 + JMP callbackasm1(SB) + MOVV $1869, R12 + JMP callbackasm1(SB) + MOVV $1870, R12 + JMP callbackasm1(SB) + MOVV $1871, R12 + JMP callbackasm1(SB) + MOVV $1872, R12 + JMP callbackasm1(SB) + MOVV $1873, R12 + JMP callbackasm1(SB) + MOVV $1874, R12 + JMP callbackasm1(SB) + MOVV $1875, R12 + JMP callbackasm1(SB) + MOVV $1876, R12 + JMP callbackasm1(SB) + MOVV $1877, R12 + JMP callbackasm1(SB) + MOVV $1878, R12 + JMP callbackasm1(SB) + MOVV $1879, R12 + JMP callbackasm1(SB) + MOVV $1880, R12 + JMP callbackasm1(SB) + MOVV $1881, R12 + JMP callbackasm1(SB) + MOVV $1882, R12 + JMP callbackasm1(SB) + MOVV $1883, R12 + JMP callbackasm1(SB) + MOVV $1884, R12 + JMP callbackasm1(SB) + MOVV $1885, R12 + JMP callbackasm1(SB) + MOVV $1886, R12 + JMP callbackasm1(SB) + MOVV $1887, R12 + JMP callbackasm1(SB) + MOVV $1888, R12 + JMP callbackasm1(SB) + MOVV $1889, R12 + JMP callbackasm1(SB) + MOVV $1890, R12 + JMP callbackasm1(SB) + MOVV $1891, R12 + JMP callbackasm1(SB) + MOVV $1892, R12 + JMP callbackasm1(SB) + MOVV $1893, R12 + JMP callbackasm1(SB) + MOVV $1894, R12 + JMP callbackasm1(SB) + MOVV $1895, R12 + JMP callbackasm1(SB) + MOVV $1896, R12 + JMP callbackasm1(SB) + MOVV $1897, R12 + JMP callbackasm1(SB) + MOVV $1898, R12 + JMP callbackasm1(SB) + MOVV $1899, R12 + JMP callbackasm1(SB) + MOVV $1900, R12 + JMP callbackasm1(SB) + MOVV $1901, R12 + JMP callbackasm1(SB) + MOVV $1902, R12 + JMP callbackasm1(SB) + MOVV $1903, R12 + JMP callbackasm1(SB) + MOVV $1904, R12 + JMP callbackasm1(SB) + MOVV $1905, R12 + JMP callbackasm1(SB) + MOVV $1906, R12 + JMP callbackasm1(SB) + MOVV $1907, R12 + JMP callbackasm1(SB) + MOVV $1908, R12 + JMP callbackasm1(SB) + MOVV $1909, R12 + JMP callbackasm1(SB) + MOVV $1910, R12 + JMP callbackasm1(SB) + MOVV $1911, R12 + JMP callbackasm1(SB) + MOVV $1912, R12 + JMP callbackasm1(SB) + MOVV $1913, R12 + JMP callbackasm1(SB) + MOVV $1914, R12 + JMP callbackasm1(SB) + MOVV $1915, R12 + JMP callbackasm1(SB) + MOVV $1916, R12 + JMP callbackasm1(SB) + MOVV $1917, R12 + JMP callbackasm1(SB) + MOVV $1918, R12 + JMP callbackasm1(SB) + MOVV $1919, R12 + JMP callbackasm1(SB) + MOVV $1920, R12 + JMP callbackasm1(SB) + MOVV $1921, R12 + JMP callbackasm1(SB) + MOVV $1922, R12 + JMP callbackasm1(SB) + MOVV $1923, R12 + JMP callbackasm1(SB) + MOVV $1924, R12 + JMP callbackasm1(SB) + MOVV $1925, R12 + JMP callbackasm1(SB) + MOVV $1926, R12 + JMP callbackasm1(SB) + MOVV $1927, R12 + JMP callbackasm1(SB) + MOVV $1928, R12 + JMP callbackasm1(SB) + MOVV $1929, R12 + JMP callbackasm1(SB) + MOVV $1930, R12 + JMP callbackasm1(SB) + MOVV $1931, R12 + JMP callbackasm1(SB) + MOVV $1932, R12 + JMP callbackasm1(SB) + MOVV $1933, R12 + JMP callbackasm1(SB) + MOVV $1934, R12 + JMP callbackasm1(SB) + MOVV $1935, R12 + JMP callbackasm1(SB) + MOVV $1936, R12 + JMP callbackasm1(SB) + MOVV $1937, R12 + JMP callbackasm1(SB) + MOVV $1938, R12 + JMP callbackasm1(SB) + MOVV $1939, R12 + JMP callbackasm1(SB) + MOVV $1940, R12 + JMP callbackasm1(SB) + MOVV $1941, R12 + JMP callbackasm1(SB) + MOVV $1942, R12 + JMP callbackasm1(SB) + MOVV $1943, R12 + JMP callbackasm1(SB) + MOVV $1944, R12 + JMP callbackasm1(SB) + MOVV $1945, R12 + JMP callbackasm1(SB) + MOVV $1946, R12 + JMP callbackasm1(SB) + MOVV $1947, R12 + JMP callbackasm1(SB) + MOVV $1948, R12 + JMP callbackasm1(SB) + MOVV $1949, R12 + JMP callbackasm1(SB) + MOVV $1950, R12 + JMP callbackasm1(SB) + MOVV $1951, R12 + JMP callbackasm1(SB) + MOVV $1952, R12 + JMP callbackasm1(SB) + MOVV $1953, R12 + JMP callbackasm1(SB) + MOVV $1954, R12 + JMP callbackasm1(SB) + MOVV $1955, R12 + JMP callbackasm1(SB) + MOVV $1956, R12 + JMP callbackasm1(SB) + MOVV $1957, R12 + JMP callbackasm1(SB) + MOVV $1958, R12 + JMP callbackasm1(SB) + MOVV $1959, R12 + JMP callbackasm1(SB) + MOVV $1960, R12 + JMP callbackasm1(SB) + MOVV $1961, R12 + JMP callbackasm1(SB) + MOVV $1962, R12 + JMP callbackasm1(SB) + MOVV $1963, R12 + JMP callbackasm1(SB) + MOVV $1964, R12 + JMP callbackasm1(SB) + MOVV $1965, R12 + JMP callbackasm1(SB) + MOVV $1966, R12 + JMP callbackasm1(SB) + MOVV $1967, R12 + JMP callbackasm1(SB) + MOVV $1968, R12 + JMP callbackasm1(SB) + MOVV $1969, R12 + JMP callbackasm1(SB) + MOVV $1970, R12 + JMP callbackasm1(SB) + MOVV $1971, R12 + JMP callbackasm1(SB) + MOVV $1972, R12 + JMP callbackasm1(SB) + MOVV $1973, R12 + JMP callbackasm1(SB) + MOVV $1974, R12 + JMP callbackasm1(SB) + MOVV $1975, R12 + JMP callbackasm1(SB) + MOVV $1976, R12 + JMP callbackasm1(SB) + MOVV $1977, R12 + JMP callbackasm1(SB) + MOVV $1978, R12 + JMP callbackasm1(SB) + MOVV $1979, R12 + JMP callbackasm1(SB) + MOVV $1980, R12 + JMP callbackasm1(SB) + MOVV $1981, R12 + JMP callbackasm1(SB) + MOVV $1982, R12 + JMP callbackasm1(SB) + MOVV $1983, R12 + JMP callbackasm1(SB) + MOVV $1984, R12 + JMP callbackasm1(SB) + MOVV $1985, R12 + JMP callbackasm1(SB) + MOVV $1986, R12 + JMP callbackasm1(SB) + MOVV $1987, R12 + JMP callbackasm1(SB) + MOVV $1988, R12 + JMP callbackasm1(SB) + MOVV $1989, R12 + JMP callbackasm1(SB) + MOVV $1990, R12 + JMP callbackasm1(SB) + MOVV $1991, R12 + JMP callbackasm1(SB) + MOVV $1992, R12 + JMP callbackasm1(SB) + MOVV $1993, R12 + JMP callbackasm1(SB) + MOVV $1994, R12 + JMP callbackasm1(SB) + MOVV $1995, R12 + JMP callbackasm1(SB) + MOVV $1996, R12 + JMP callbackasm1(SB) + MOVV $1997, R12 + JMP callbackasm1(SB) + MOVV $1998, R12 + JMP callbackasm1(SB) + MOVV $1999, R12 + JMP callbackasm1(SB) diff --git a/vendor/github.com/fredbi/uri/.gitignore b/vendor/github.com/fredbi/uri/.gitignore new file mode 100644 index 0000000..24d90e1 --- /dev/null +++ b/vendor/github.com/fredbi/uri/.gitignore @@ -0,0 +1,3 @@ +coverage.txt +*.out +*.test diff --git a/vendor/github.com/fredbi/uri/.golangci.yml b/vendor/github.com/fredbi/uri/.golangci.yml new file mode 100644 index 0000000..8dd8bc4 --- /dev/null +++ b/vendor/github.com/fredbi/uri/.golangci.yml @@ -0,0 +1,65 @@ +version: "2" +linters: + default: all + disable: + - cyclop + - depguard + - errchkjson + - errorlint + - exhaustruct + - forcetypeassert + - funlen + - gochecknoglobals + - gochecknoinits + - gocognit + - godot + - godox + - gosmopolitan + - inamedparam + - ireturn + - lll + - musttag + - noinlineerr + - nestif + - nlreturn + - nonamedreturns + - paralleltest + - recvcheck + - testpackage + - thelper + - tparallel + - unparam + - varnamelen + - whitespace + - wrapcheck + - wsl + - wsl_v5 + settings: + dupl: + threshold: 200 + goconst: + min-len: 2 + min-occurrences: 3 + gocyclo: + min-complexity: 55 + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - gofmt + - goimports + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/vendor/github.com/fredbi/uri/LICENSE.md b/vendor/github.com/fredbi/uri/LICENSE.md new file mode 100644 index 0000000..8e01e30 --- /dev/null +++ b/vendor/github.com/fredbi/uri/LICENSE.md @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2018 Frederic Bidon +Copyright (c) 2015 Trey Tacon + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. diff --git a/vendor/github.com/fredbi/uri/README.md b/vendor/github.com/fredbi/uri/README.md new file mode 100644 index 0000000..f93a541 --- /dev/null +++ b/vendor/github.com/fredbi/uri/README.md @@ -0,0 +1,244 @@ +# uri +![Lint](https://github.com/fredbi/uri/actions/workflows/01-golang-lint.yaml/badge.svg) +![CI](https://github.com/fredbi/uri/actions/workflows/02-test.yaml/badge.svg) +[![Coverage Status](https://coveralls.io/repos/github/fredbi/uri/badge.svg?branch=master)](https://coveralls.io/github/fredbi/uri?branch=master) +![Vulnerability Check](https://github.com/fredbi/uri/actions/workflows/03-govulncheck.yaml/badge.svg) +[![Go Report Card](https://goreportcard.com/badge/github.com/fredbi/uri)](https://goreportcard.com/report/github.com/fredbi/uri) + +![GitHub tag (latest by date)](https://img.shields.io/github/v/tag/fredbi/uri) +[![Go Reference](https://pkg.go.dev/badge/github.com/fredbi/uri.svg)](https://pkg.go.dev/github.com/fredbi/uri) +[![license](http://img.shields.io/badge/license/License-MIT-yellow.svg)](https://raw.githubusercontent.com/fredbi/uri/master/LICENSE.md) + + +Package `uri` is meant to be an RFC 3986 compliant URI builder, parser and validator for `golang`. + +It supports strict RFC validation for URIs and URI relative references. + +This allows for stricter conformance than the `net/url` package in the `go` standard libary, +which provides a workable but loose implementation of the RFC for URLs. + +**Requires go1.19**. + +## What's new? + +### V1.2 announcement + +To do before I cut a v1.2.0: +* [] handle empty fragment, empty query. + Ex: `https://host?` is not equivalent to `http://host`. + Similarly `https://host#` is not equivalent to `http://host`. +* [] IRI UCS charset compliance +* [] URI normalization (like [PuerkitoBio/purell](https://github.com/PuerkitoBio/purell)) +* [] more explicit errors, with context + +See also [TODOs](./docs/TODO.md). + +### V2 announcement + +V2 is getting closer to completion. It comes with: +* very significant performance improvement (x 1.5). + Eventually `uri` gets significantly faster than `net/url` (-50% ns/op) +* a simplified API: no interface, no `Validate()`, no `Builder()` +* options for tuning validation strictness +* exported package level variables disappear + +### Current master (unreleased) + +**Fixes** + +* stricter scheme validation (no longer support non-ASCII letters). + Ex: `Smørrebrød://` is not a valid scheme. +* stricter IP validation (do not support escaping in addresses, excepted for IPv6 zones) +* stricter percent-escape validation: an escaped character **MUST** decode to a valid UTF8 endpoint (1). + Ex: %C3 is an incomplete escaped UTF8 sequence. Should be %C3%B8 to escape the full UTF8 rune. +* stricter port validation. A port is an integer less than or equal to 65535. + +> (1) +> `go` natively manipulates UTF8 strings only. Even though the standards are not strict about the character +> encoding of escaped sequences, it seems natural to prevent invalid UTF8 to propagate via percent escaping. +> Notice that this approach is not the one followed by `net/url.PathUnescape()`, which leaves invalid runes. + +**Features** + +* feat: added `IsIP()` bool and `IPAddr() netip.Addr` methods + +**Performances** + +* perf: slight improvement. Now only 8-25% slower than net/url.Parse, depending on the workload + +### [Older releases](#release-notes) + +## Usage + +### Parsing + +```go + u, err := Parse("https://example.com:8080/path") + if err != nil { + fmt.Printf("Invalid URI") + } else { + fmt.Printf("%s", u.Scheme()) + } + // Output: https +``` + +```go + u, err := ParseReference("//example.com/path") + if err != nil { + fmt.Printf("Invalid URI reference") + } else { + fmt.Printf("%s", u.Authority().Path()) + } + // Output: /path +``` + +### Validating + +```go + isValid := IsURI("urn://example.com?query=x#fragment/path") // true + + isValid= IsURI("//example.com?query=x#fragment/path") // false + + isValid= IsURIReference("//example.com?query=x#fragment/path") // true +``` + +#### Caveats + +* **Registered name vs DNS name**: RFC3986 defines a super-permissive "registered name" for hosts, for URIs + not specifically related to an Internet name. Our validation performs a stricter host validation according + to DNS rules whenever the scheme is a well-known IANA-registered scheme + (the function `UsesDNSHostValidation(string) bool` is customizable). + +> Examples: +> `ftp://host`, `http://host` default to validating a proper DNS hostname. + +* **IPv6 validation** relies on IP parsing from the standard library. It is not super strict + regarding the full-fledged IPv6 specification, but abides by the URI RFC's. + +* **URI vs URL**: every URL should be a URI, but the converse does not always hold. This module intends to perform + stricter validation than the pragmatic standard library `net/url`, which currently remains about 30% faster. + +* **URI vs IRI**: at this moment, this module checks for URI, while supporting unicode letters as `ALPHA` tokens. + This is not strictly compliant with the IRI specification (see known issues). + +### Building + +The exposed type `URI` can be transformed into a fluent `Builder` to set the parts of an URI. + +```go + aURI, _ := Parse("mailto://user@domain.com") + newURI := auri.Builder().SetUserInfo(test.name).SetHost("newdomain.com").SetScheme("http").SetPort("443") +``` + +### Canonicalization + +Not supported for now (contemplated as a topic for V2). + +For URL normalization, see [PuerkitoBio/purell](https://github.com/PuerkitoBio/purell). + +## Reference specifications + +The librarian's corner (still WIP). + +|Title|Reference|Notes| +|---------------------------------------------|-------------------------------------------------------|----------------| +| Uniform Resource Identifier (URI) | [RFC3986](https://www.rfc-editor.org/rfc/rfc3986) | (1)(2) | +| Uniform Resource Locator (URL) | [RFC1738](https://www.rfc-editor.org/info/rfc1738) | | +| Relative URL | [RFC1808](https://www.rfc-editor.org/info/rfc1808) | | +| Internationalized Resource Identifier (IRI) | [RFC3987](https://tools.ietf.org/html/rfc3987) | (1) | +| Practical standardization guidelines | [URL WhatWG Living Standard](https://url.spec.whatwg.org/) |(4)| +| Domain names implementation | [RFC1035](https://datatracker.ietf.org/doc/html/rfc1035) || +|||| +| **IPv6** ||| +| Representing IPv6 Zone Identifiers | [RFC6874](https://www.rfc-editor.org/rfc/rfc6874.txt) | | | +| IPv6 Addressing architecture | [RFC3513](https://www.rfc-editor.org/rfc/rfc3513.txt) || +| **URI Schemes** ||| +|||| +| IANA registered URI schemes | [IANA](https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml) | (5) | +|||| +| **Port numbers** ||| +| IANA port assignments by service | [IANA](https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.txt) || +| Well-known TCP and UDP port numbers | [Wikipedia)(https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers) || +| + +**Notes** + +(1) Deviations from the RFC: +* Tokens: ALPHAs are tolerated to be Unicode Letter codepoints. Schemes remain constrained to ASCII letters (`[a-z]|[A-Z]`) +* DIGITs are ASCII digits as required by the RFC. Unicode Digit codepoints are rejected (ex: ६ (6), ① , 六 (6), Ⅶ (7) are not considered legit URI DIGITS). + +> Some improvements are still needed to abide more strictly to IRI's provisions for internationalization. Working on it... + +(2) Percent-escape: +* Escape sequences, e.g. `%hh` _must_ decode to valid UTF8 runes (standard says _should_). + +(2) IP addresses: +* As per RFC3986, `[hh::...]` literals _must_ be IPv6 and `ddd.ddd.ddd.ddd` litterals _must_ be IPv4. +* As per RFC3986, notice that `[]` is illegal, although the golang IP parser translates this to `[::]` (zero value IP). + In `go`, the zero value for `netip.Addr` is invalid just a well. +* IPv6 zones are supported, with the '%' escaped as '%25' to denote an IPv6 zoneID (RFC6974) +* IPvFuture addresses _are_ supported, with escape sequences (which are not part of RFC3986, but natural since IPv6 do support escaping) + +(4) Deviations from the WhatWG recommendation +* `[]` IPv6 address is invalid +* invalid percent-encoded characters trigger an error rather than being ignored + +(5) Most _permanently_ registered schemes have been accounted for when checking whether Domain Names apply for hosts rather than the + "registered name" from RFC3986. Quite a few commonly used found, either unregistered or with a provisional status have been added as well. + Feel free to create an issue or contribute a change to enrich this list of well-known URI schemes. + +## [FAQ](docs/FAQ.md) + +## [Benchmarks](docs/BENCHMARKS.md) + +## Credits + +* Tests have been aggregated from the test suites of URI validators from other languages: +Perl, Python, Scala, .Net. and the Go url standard library. + +* This package was initially based on the work from ttacon/uri (credits: Trey Tacon). +> Extra features like MySQL URIs present in the original repo have been removed. + +* A lot of improvements and suggestions have been brought by the incredible guys at [`fyne-io`](https://github.com/fyne-io). Thanks all. + +## Release notes + +### v1.1.0 + +**Build** + +* requires go1.19 + +**Features** + +* Typed errors: parsing and validation now returns errors of type `uri.Error`, + with a more accurate pinpointing of the error provided by the value. + Errors support the go1.20 addition to standard errors with `Join()` and `Cause()`. + For go1.19, backward compatibility is ensured (errors.Join() is emulated). +* DNS schemes can be overridden at the package level + +**Performances** + +* Significantly improved parsing speed by dropping usage of regular expressions and reducing allocations (~ x20 faster). + +**Fixes** + +* stricter compliance regarding paths beginning with a double '/' +* stricter compliance regarding the length of DNS names and their segments +* stricter compliance regarding IPv6 addresses with an empty zone +* stricter compliance regarding IPv6 vs IPv4 litterals +* an empty IPv6 litteral `[]` is invalid + +**Known open issues** + +* IRI validation lacks strictness +* IPv6 validation relies on the standard library and lacks strictness + +**Other** + +Major refactoring to enhance code readability, esp. for testing code. + +* Refactored validations +* Refactored test suite +* Added support for fuzzing, dependabots & codeQL scans + diff --git a/vendor/github.com/fredbi/uri/builder.go b/vendor/github.com/fredbi/uri/builder.go new file mode 100644 index 0000000..8378ba5 --- /dev/null +++ b/vendor/github.com/fredbi/uri/builder.go @@ -0,0 +1,59 @@ +package uri + +// Builder builds URIs. +type Builder interface { + URI() URI + SetScheme(scheme string) Builder + SetUserInfo(userinfo string) Builder + SetHost(host string) Builder + SetPort(port string) Builder + SetPath(path string) Builder + SetQuery(query string) Builder + SetFragment(fragment string) Builder + + // Returns the URI this Builder represents. + String() string +} + +func (u *uri) SetScheme(scheme string) Builder { + u.scheme = scheme + return u +} + +func (u *uri) SetUserInfo(userinfo string) Builder { + u.ensureAuthorityExists() + u.authority.userinfo = userinfo + return u +} + +func (u *uri) SetHost(host string) Builder { + u.ensureAuthorityExists() + u.authority.host = host + return u +} + +func (u *uri) SetPort(port string) Builder { + u.ensureAuthorityExists() + u.authority.port = port + return u +} + +func (u *uri) SetPath(path string) Builder { + u.ensureAuthorityExists() + u.authority.path = path + return u +} + +func (u *uri) SetQuery(query string) Builder { + u.query = query + return u +} + +func (u *uri) SetFragment(fragment string) Builder { + u.fragment = fragment + return u +} + +func (u *uri) Builder() Builder { + return u +} diff --git a/vendor/github.com/fredbi/uri/decode.go b/vendor/github.com/fredbi/uri/decode.go new file mode 100644 index 0000000..85d2197 --- /dev/null +++ b/vendor/github.com/fredbi/uri/decode.go @@ -0,0 +1,206 @@ +package uri + +import ( + "fmt" + "strings" + "unicode" + "unicode/utf8" +) + +func validateUnreservedWithExtra(s string, acceptedRunes []rune) error { + for i := 0; i < len(s); { + r, size := utf8.DecodeRuneInString(s[i:]) + if r == utf8.RuneError { + return errorsJoin(ErrInvalidEscaping, + fmt.Errorf("invalid UTF8 rune near: %q: %w", s[i:], ErrURI), + ) + } + i += size + + // accepts percent-encoded sequences, but only if they correspond to a valid UTF-8 encoding + if r == percentMark { + if i >= len(s) { + return errorsJoin(ErrInvalidEscaping, + fmt.Errorf("incomplete escape sequence: %w", ErrURI), + ) + } + + _, offset, err := unescapePercentEncoding(s[i:]) + if err != nil { + return errorsJoin(ErrInvalidEscaping, err) + } + + i += offset + + continue + } + + // RFC grammar definitions: + // sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + // / "*" / "+" / "," / ";" / "=" + // gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" + // unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" + if !unicode.IsLetter(r) && !unicode.IsDigit(r) && + // unreserved + r != '-' && r != '.' && r != '_' && r != '~' && + // sub-delims + r != '!' && r != '$' && r != '&' && r != '\'' && r != '(' && r != ')' && + r != '*' && r != '+' && r != ',' && r != ';' && r != '=' { + runeFound := false + for _, acceptedRune := range acceptedRunes { + if r == acceptedRune { + runeFound = true + break + } + } + + if !runeFound { + return fmt.Errorf("contains an invalid character: '%U' (%q) near %q: %w", r, r, s[i:], ErrURI) + } + } + } + + return nil +} + +func unescapePercentEncoding(s string) (rune, int, error) { + var ( + offset int + codePoint [utf8.UTFMax]byte + codePointLength int + err error + ) + + if codePoint[0], err = unescapeSequence(s); err != nil { + return utf8.RuneError, 0, err + } + + codePointLength++ + offset += 2 + const ( + twoBytesUnicodePoint = 0b11000000 + threeBytesUnicodePoint = 0b11100000 + fourBytesUnicodePoint = 0b11110000 + ) + + // escaped utf8 sequence + if codePoint[0] >= twoBytesUnicodePoint { + // expect another escaped sequence + if offset >= len(s) { + return 0, 0, fmt.Errorf("expected a '%%' escape character, near: %q: %w", s, ErrURI) + } + + if s[offset] != '%' { + return 0, 0, fmt.Errorf("expected a '%%' escape character, near: %q: %w", s[offset:], ErrURI) + } + offset++ + + if codePoint[1], err = unescapeSequence(s[offset:]); err != nil { + return utf8.RuneError, 0, err + } + + codePointLength++ + offset += 2 + + if codePoint[0] >= threeBytesUnicodePoint { + // expect yet another escaped sequence + if offset >= len(s) { + return 0, 0, fmt.Errorf("expected a '%%' escape character, near: %q: %w", s, ErrURI) + } + + if s[offset] != '%' { + return 0, 0, fmt.Errorf("expected a '%%' escape character, near: %q: %w", s[offset:], ErrURI) + } + offset++ + + if codePoint[2], err = unescapeSequence(s[offset:]); err != nil { + return utf8.RuneError, 0, err + } + codePointLength++ + offset += 2 + + if codePoint[0] >= fourBytesUnicodePoint { + // expect a fourth escaped sequence + if offset >= len(s) { + return 0, 0, fmt.Errorf("expected a '%%' escape character, near: %q: %w", s, ErrURI) + } + + if s[offset] != '%' { + return 0, 0, fmt.Errorf("expected a '%%' escape character, near: %q: %w", s[offset:], ErrURI) + } + offset++ + + if codePoint[3], err = unescapeSequence(s[offset:]); err != nil { + return utf8.RuneError, 0, err + } + codePointLength++ + offset += 2 + } + } + } + + unescapedRune, _ := utf8.DecodeRune(codePoint[:codePointLength]) + if unescapedRune == utf8.RuneError { + return utf8.RuneError, 0, fmt.Errorf("the escaped code points do not add up to a valid rune: %w", ErrURI) + } + + return unescapedRune, offset, nil +} + +func unescapeSequence(escapeSequence string) (byte, error) { + const ( + minEscapeSequenceLength = 2 + ) + if len(escapeSequence) < minEscapeSequenceLength { + return 0, fmt.Errorf("expected escaping '%%' to be followed by 2 hex digits, near: %q: %w", escapeSequence, ErrURI) + } + + if !isHex(escapeSequence[0]) || !isHex(escapeSequence[1]) { + return 0, fmt.Errorf("part contains a malformed percent-encoded hex digit, near: %q: %w", escapeSequence, ErrURI) + } + + return unhex(escapeSequence[0])<<4 | unhex(escapeSequence[1]), nil +} + +func isHex[T byte | rune](c T) bool { + switch { + case isDigit(c): + return true + case 'a' <= c && c <= 'f': + return true + case 'A' <= c && c <= 'F': + return true + default: + return false + } +} + +func isNotDigit[T rune | byte](r T) bool { + return r < '0' || r > '9' +} + +func isDigit[T rune | byte](r T) bool { + return r >= '0' && r <= '9' +} + +func isASCIILetter[T byte | rune](c T) bool { + return 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' +} + +func isNumerical(input string) bool { + return strings.IndexFunc(input, isNotDigit[rune]) == -1 +} + +func unhex(c byte) byte { + //nolint:mnd // there is no magic here: transforming a hex value in ASCII into its value + switch { + case '0' <= c && c <= '9': + return c - '0' + case 'a' <= c && c <= 'f': + return c - 'a' + 10 + case 'A' <= c && c <= 'F': + return c - 'A' + 10 + } + return 0 +} diff --git a/vendor/github.com/fredbi/uri/default_ports.go b/vendor/github.com/fredbi/uri/default_ports.go new file mode 100644 index 0000000..ba10e58 --- /dev/null +++ b/vendor/github.com/fredbi/uri/default_ports.go @@ -0,0 +1,131 @@ +package uri + +import ( + "strconv" + "strings" +) + +// IsDefaultPort indicates if the port is specified and is different from +// the defaut port defined for this scheme (if any). +// +// For example, an URI like http://host:8080 would return false, since 80 is the default http port. +func (u uri) IsDefaultPort() bool { + if len(u.authority.port) == 0 { + return true + } + + portNum, _ := strconv.ParseUint(u.authority.port, 10, 64) + + return defaultPortForScheme(strings.ToLower(u.scheme)) == portNum +} + +// DefaultPort returns the default standardized port for the scheme of this URI, +// or zero if no such default is known. +// +// For example, for scheme "https", the default port is 443. +func (u uri) DefaultPort() int { + return int(defaultPortForScheme(strings.ToLower(u.scheme))) //nolint:gosec // uint64 -> int conversion is ok: no port overflows a int +} + +// References: +// * https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml +// * https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml +// +// Also: https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers +func defaultPortForScheme(scheme string) uint64 { + //nolint:mnd // no need to define default ports with additional constants + switch scheme { + case "aaa": + return 3868 + case "aaas": + return 5658 + case "acap": + return 674 + case "cap": + return 1026 + case "coap", "coap+tcp": + return 5683 + case "coaps": + return 5684 + case "coap+ws": + return 80 + case "coaps+ws": + return 443 + case "dict": + return 2628 + case "dns": + return 53 + case "finger": + return 79 + case "ftp": + return 21 + case "git": + return 9418 + case "go": + return 1096 + case "gopher": + return 70 + case "http": + return 80 + case "https": + return 443 + case "iax": + return 4569 + case "icap": + return 1344 + case "imap": + return 143 + case "ipp", "ipps": + return 631 + case "irc", "irc6": + return 6667 + case "ircs": + return 6697 + case "ldap": + return 389 + case "mailto": + return 25 + case "msrp", "msrps": + return 2855 + case "nfs": + return 2049 + case "nntp": + return 119 + case "ntp": + return 123 + case "postgresql": + return 5432 + case "radius": + return 1812 + case "redis": + return 6379 + case "rmi": + return 1098 + case "rtsp", "rtsps", "rtspu": + return 554 + case "rsync": + return 873 + case "sftp": + return 22 + case "skype": + return 23399 + case "smtp": + return 25 + case "snmp": + return 161 + case "ssh": + return 22 + case "steam": + return 7777 + case "svn": + return 3690 + case "telnet": + return 23 + case "vnc": + return 5500 + case "wss": + return 6602 + } + + return 0 +} diff --git a/vendor/github.com/fredbi/uri/dns.go b/vendor/github.com/fredbi/uri/dns.go new file mode 100644 index 0000000..456e3d3 --- /dev/null +++ b/vendor/github.com/fredbi/uri/dns.go @@ -0,0 +1,318 @@ +package uri + +import ( + "fmt" + "strconv" + "unicode" + "unicode/utf8" +) + +// UsesDNSHostValidation returns true if the provided scheme has host validation +// that does not follow RFC3986 (which is quite generic), and assumes a valid +// DNS hostname instead. +// +// This function is declared as a global variable that may be overridden at the package level, +// in case you need specific schemes to validate the host as a DNS name. +// +// See: https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml +var UsesDNSHostValidation = func(scheme string) bool { + switch scheme { + // prioritize early exit on most commonly used schemes + case "https", "http": + return true + case "file": + return false + // less commonly used schemes + case "aaa": + return true + case "aaas": + return true + case "acap": + return true + case "acct": + return true + case "cap": + return true + case "cid": + return true + case "coap", "coaps", "coap+tcp", "coap+ws", "coaps+tcp", "coaps+ws": + return true + case "dav": + return true + case "dict": + return true + case "dns": + return true + case "dntp": + return true + case "finger": + return true + case "ftp": + return true + case "git": + return true + case "gopher": + return true + case "h323": + return true + case "iax": + return true + case "icap": + return true + case "im": + return true + case "imap": + return true + case "ipp", "ipps": + return true + case "irc", "irc6", "ircs": + return true + case "jms": + return true + case "ldap": + return true + case "mailto": + return true + case "mid": + return true + case "msrp", "msrps": + return true + case "nfs": + return true + case "nntp": + return true + case "ntp": + return true + case "postgresql": + return true + case "radius": + return true + case "redis": + return true + case "rmi": + return true + case "rtsp", "rtsps", "rtspu": + return true + case "rsync": + return true + case "sftp": + return true + case "skype": + return true + case "smtp": + return true + case "snmp": + return true + case "soap": + return true + case "ssh": + return true + case "steam": + return true + case "svn": + return true + case "tcp": + return true + case "telnet": + return true + case "udp": + return true + case "vnc": + return true + case "wais": + return true + case "ws": + return true + case "wss": + return true + } + + return false +} + +func validateDNSHostForScheme(host string) error { + // ref: https://datatracker.ietf.org/doc/html/rfc1035 + // ::= | " " + // ::=