diff --git a/docs/src/content/docs/learn/badges.mdx b/docs/src/content/docs/learn/badges.mdx index a1406cbf8..a40597dcd 100644 --- a/docs/src/content/docs/learn/badges.mdx +++ b/docs/src/content/docs/learn/badges.mdx @@ -74,6 +74,30 @@ badgeService.SetBadge("3") badgeService.SetBadge("New") ``` +### Setting a Custom Badge + +Set a badge on the application tile/dock icon with one-off options applied: + +#### Go +```go +options := badge.Options{ + BackgroundColour: color.RGBA{0, 255, 255, 255}, + FontName: "arialb.ttf", // System font + FontSize: 16, + SmallFontSize: 10, + TextColour: color.RGBA{0, 0, 0, 255}, +} + +// Set a default badge +badgeService.SetCustomBadge("", options) + +// Set a numeric badge +badgeService.SetCustomBadge("3", options) + +// Set a text badge +badgeService.SetCustomBadge("New", options) +``` + ### Removing a Badge Remove the badge from the application icon: diff --git a/v3/examples/badge-custom/README.md b/v3/examples/badge-custom/README.md index 8cf0d9d37..ab4c5a3fb 100644 --- a/v3/examples/badge-custom/README.md +++ b/v3/examples/badge-custom/README.md @@ -35,7 +35,7 @@ app := application.New(application.Options{ ### Setting a Badge -Set a badge on the application tile/dock icon: +Set a badge on the application tile/dock icon with the global options applied: #### Go ```go @@ -63,6 +63,54 @@ SetBadge("3") SetBadge("New") ``` +### Setting a Custom Badge + +Set a badge on the application tile/dock icon with one-off options applied: + +#### Go +```go +// Set a default badge +badgeService.SetCustomBadge("") + +// Set a numeric badge +badgeService.SetCustomBadge("3") + +// Set a text badge +badgeService.SetCustomBadge("New") +``` + +#### JS +```js +import {SetCustomBadge} from "../bindings/github.com/wailsapp/wails/v3/pkg/services/badge/service"; + +const options = { + BackgroundColour: RGBA.createFrom({ + R: 0, + G: 255, + B: 255, + A: 255, + }), + FontName: "arialb.ttf", // System font + FontSize: 16, + SmallFontSize: 10, + TextColour: RGBA.createFrom({ + R: 0, + G: 0, + B: 0, + A: 255, + }), +} + +// Set a default badge +SetCustomBadge("", options) + +// Set a numeric badge +SetCustomBadge("3", options) + +// Set a text badge +SetCustomBadge("New", options) +``` + ### Removing a Badge Remove the badge from the application icon: diff --git a/v3/examples/badge-custom/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/badge/index.ts b/v3/examples/badge-custom/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/badge/index.ts index fd900b4cd..3c5d48043 100644 --- a/v3/examples/badge-custom/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/badge/index.ts +++ b/v3/examples/badge-custom/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/badge/index.ts @@ -5,3 +5,7 @@ import * as Service from "./service.js"; export { Service }; + +export { + Options +} from "./models.js"; diff --git a/v3/examples/badge-custom/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/badge/models.ts b/v3/examples/badge-custom/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/badge/models.ts new file mode 100644 index 000000000..67ed264c0 --- /dev/null +++ b/v3/examples/badge-custom/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/badge/models.ts @@ -0,0 +1,58 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import { Create as $Create } from "@wailsio/runtime"; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import * as color$0 from "../../../../../../../image/color/models.js"; + +export class Options { + "TextColour": color$0.RGBA; + "BackgroundColour": color$0.RGBA; + "FontName": string; + "FontSize": number; + "SmallFontSize": number; + + /** Creates a new Options instance. */ + constructor($$source: Partial = {}) { + if (!("TextColour" in $$source)) { + this["TextColour"] = (new color$0.RGBA()); + } + if (!("BackgroundColour" in $$source)) { + this["BackgroundColour"] = (new color$0.RGBA()); + } + if (!("FontName" in $$source)) { + this["FontName"] = ""; + } + if (!("FontSize" in $$source)) { + this["FontSize"] = 0; + } + if (!("SmallFontSize" in $$source)) { + this["SmallFontSize"] = 0; + } + + Object.assign(this, $$source); + } + + /** + * Creates a new Options instance from a string or object. + */ + static createFrom($$source: any = {}): Options { + const $$createField0_0 = $$createType0; + const $$createField1_0 = $$createType0; + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + if ("TextColour" in $$parsedSource) { + $$parsedSource["TextColour"] = $$createField0_0($$parsedSource["TextColour"]); + } + if ("BackgroundColour" in $$parsedSource) { + $$parsedSource["BackgroundColour"] = $$createField1_0($$parsedSource["BackgroundColour"]); + } + return new Options($$parsedSource as Partial); + } +} + +// Private type creation functions +const $$createType0 = color$0.RGBA.createFrom; diff --git a/v3/examples/badge-custom/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/badge/service.ts b/v3/examples/badge-custom/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/badge/service.ts index 2f3ee30a6..dca83282b 100644 --- a/v3/examples/badge-custom/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/badge/service.ts +++ b/v3/examples/badge-custom/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/badge/service.ts @@ -10,6 +10,10 @@ // @ts-ignore: Unused imports import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime"; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import * as $models from "./models.js"; + /** * RemoveBadge removes the badge label from the application icon. */ @@ -23,3 +27,7 @@ export function RemoveBadge(): $CancellablePromise { export function SetBadge(label: string): $CancellablePromise { return $Call.ByID(3052354152, label); } + +export function SetCustomBadge(label: string, options: $models.Options): $CancellablePromise { + return $Call.ByID(921166821, label, options); +} diff --git a/v3/examples/badge-custom/frontend/bindings/image/color/index.ts b/v3/examples/badge-custom/frontend/bindings/image/color/index.ts new file mode 100644 index 000000000..97b507b08 --- /dev/null +++ b/v3/examples/badge-custom/frontend/bindings/image/color/index.ts @@ -0,0 +1,6 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export { + RGBA +} from "./models.js"; diff --git a/v3/examples/badge-custom/frontend/bindings/image/color/models.ts b/v3/examples/badge-custom/frontend/bindings/image/color/models.ts new file mode 100644 index 000000000..0d4eab56d --- /dev/null +++ b/v3/examples/badge-custom/frontend/bindings/image/color/models.ts @@ -0,0 +1,46 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import { Create as $Create } from "@wailsio/runtime"; + +/** + * RGBA represents a traditional 32-bit alpha-premultiplied color, having 8 + * bits for each of red, green, blue and alpha. + * + * An alpha-premultiplied color component C has been scaled by alpha (A), so + * has valid values 0 <= C <= A. + */ +export class RGBA { + "R": number; + "G": number; + "B": number; + "A": number; + + /** Creates a new RGBA instance. */ + constructor($$source: Partial = {}) { + if (!("R" in $$source)) { + this["R"] = 0; + } + if (!("G" in $$source)) { + this["G"] = 0; + } + if (!("B" in $$source)) { + this["B"] = 0; + } + if (!("A" in $$source)) { + this["A"] = 0; + } + + Object.assign(this, $$source); + } + + /** + * Creates a new RGBA instance from a string or object. + */ + static createFrom($$source: any = {}): RGBA { + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + return new RGBA($$parsedSource as Partial); + } +} diff --git a/v3/examples/badge-custom/frontend/dist/assets/index-edhLCYCH.js b/v3/examples/badge-custom/frontend/dist/assets/index-DHsC0KxN.js similarity index 96% rename from v3/examples/badge-custom/frontend/dist/assets/index-edhLCYCH.js rename to v3/examples/badge-custom/frontend/dist/assets/index-DHsC0KxN.js index f23f80e54..98bdf5437 100644 --- a/v3/examples/badge-custom/frontend/dist/assets/index-edhLCYCH.js +++ b/v3/examples/badge-custom/frontend/dist/assets/index-DHsC0KxN.js @@ -1,3 +1,6 @@ +var __defProp = Object.defineProperty; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); (function polyfill() { const relList = document.createElement("link").relList; if (relList && relList.supports && relList.supports("modulepreload")) { @@ -1329,12 +1332,67 @@ function RemoveBadge() { function SetBadge(label) { return ByID(3052354152, label); } +function SetCustomBadge(label, options) { + return ByID(921166821, label, options); +} +class RGBA { + /** Creates a new RGBA instance. */ + constructor($$source = {}) { + __publicField(this, "R"); + __publicField(this, "G"); + __publicField(this, "B"); + __publicField(this, "A"); + if (!("R" in $$source)) { + this["R"] = 0; + } + if (!("G" in $$source)) { + this["G"] = 0; + } + if (!("B" in $$source)) { + this["B"] = 0; + } + if (!("A" in $$source)) { + this["A"] = 0; + } + Object.assign(this, $$source); + } + /** + * Creates a new RGBA instance from a string or object. + */ + static createFrom($$source = {}) { + let $$parsedSource = typeof $$source === "string" ? JSON.parse($$source) : $$source; + return new RGBA($$parsedSource); + } +} +const setCustomButton = document.getElementById("set-custom"); const setButton = document.getElementById("set"); const removeButton = document.getElementById("remove"); const setButtonUsingGo = document.getElementById("set-go"); const removeButtonUsingGo = document.getElementById("remove-go"); const labelElement = document.getElementById("label"); const timeElement = document.getElementById("time"); +setCustomButton.addEventListener("click", () => { + console.log("click!"); + let label = labelElement.value; + SetCustomBadge(label, { + BackgroundColour: RGBA.createFrom({ + R: 0, + G: 255, + B: 255, + A: 255 + }), + FontName: "arialb.ttf", + // System font + FontSize: 16, + SmallFontSize: 10, + TextColour: RGBA.createFrom({ + R: 0, + G: 0, + B: 0, + A: 255 + }) + }); +}); setButton.addEventListener("click", () => { let label = labelElement.value; SetBadge(label); diff --git a/v3/examples/badge-custom/frontend/dist/index.html b/v3/examples/badge-custom/frontend/dist/index.html index 3ea98fdf3..70886cf91 100644 --- a/v3/examples/badge-custom/frontend/dist/index.html +++ b/v3/examples/badge-custom/frontend/dist/index.html @@ -6,7 +6,7 @@ Wails App - +
@@ -23,6 +23,7 @@
+ diff --git a/v3/examples/badge-custom/frontend/index.html b/v3/examples/badge-custom/frontend/index.html index 616cb4c0f..e4a9ec4a2 100644 --- a/v3/examples/badge-custom/frontend/index.html +++ b/v3/examples/badge-custom/frontend/index.html @@ -22,6 +22,7 @@
+ diff --git a/v3/examples/badge-custom/frontend/src/main.ts b/v3/examples/badge-custom/frontend/src/main.ts index 593582b4a..82ab24eef 100644 --- a/v3/examples/badge-custom/frontend/src/main.ts +++ b/v3/examples/badge-custom/frontend/src/main.ts @@ -1,6 +1,8 @@ import {Events} from "@wailsio/runtime"; -import {SetBadge, RemoveBadge} from "../bindings/github.com/wailsapp/wails/v3/pkg/services/badge/service"; +import {SetBadge, RemoveBadge, SetCustomBadge} from "../bindings/github.com/wailsapp/wails/v3/pkg/services/badge/service"; +import { RGBA } from "../bindings/image/color/models"; +const setCustomButton = document.getElementById('set-custom')! as HTMLButtonElement; const setButton = document.getElementById('set')! as HTMLButtonElement; const removeButton = document.getElementById('remove')! as HTMLButtonElement; const setButtonUsingGo = document.getElementById('set-go')! as HTMLButtonElement; @@ -8,6 +10,28 @@ const removeButtonUsingGo = document.getElementById('remove-go')! as HTMLButtonE const labelElement : HTMLInputElement = document.getElementById('label')! as HTMLInputElement; const timeElement = document.getElementById('time')! as HTMLDivElement; +setCustomButton.addEventListener('click', () => { + console.log("click!") + let label = (labelElement as HTMLInputElement).value + SetCustomBadge(label, { + BackgroundColour: RGBA.createFrom({ + R: 0, + G: 255, + B: 255, + A: 255, + }), + FontName: "arialb.ttf", // System font + FontSize: 16, + SmallFontSize: 10, + TextColour: RGBA.createFrom({ + R: 0, + G: 0, + B: 0, + A: 255, + }), + }); +}) + setButton.addEventListener('click', () => { let label = (labelElement as HTMLInputElement).value SetBadge(label); diff --git a/v3/pkg/services/badge/badge.go b/v3/pkg/services/badge/badge.go index e178a8cb1..8d1d926d1 100644 --- a/v3/pkg/services/badge/badge.go +++ b/v3/pkg/services/badge/badge.go @@ -13,6 +13,7 @@ type platformBadge interface { Shutdown() error SetBadge(label string) error + SetCustomBadge(label string, options Options) error RemoveBadge() error } @@ -49,6 +50,10 @@ func (b *Service) SetBadge(label string) error { return b.impl.SetBadge(label) } +func (b *Service) SetCustomBadge(label string, options Options) error { + return b.impl.SetCustomBadge(label, options) +} + // RemoveBadge removes the badge label from the application icon. func (b *Service) RemoveBadge() error { return b.impl.RemoveBadge() diff --git a/v3/pkg/services/badge/badge_darwin.go b/v3/pkg/services/badge/badge_darwin.go index 9b72dfd2b..75bf04e7c 100644 --- a/v3/pkg/services/badge/badge_darwin.go +++ b/v3/pkg/services/badge/badge_darwin.go @@ -61,6 +61,12 @@ func (d *darwinBadge) SetBadge(label string) error { return nil } +// SetCustomBadge is not supported on macOS, SetBadge is called instead. +// (Windows-specific) +func (d *darwinBadge) SetCustomBadge(label string, options Options) error { + return d.SetBadge(label) +} + // RemoveBadge removes the badge label from the application icon. func (d *darwinBadge) RemoveBadge() error { C.setBadge(nil) diff --git a/v3/pkg/services/badge/badge_windows.go b/v3/pkg/services/badge/badge_windows.go index d890c6fd0..ae09edc94 100644 --- a/v3/pkg/services/badge/badge_windows.go +++ b/v3/pkg/services/badge/badge_windows.go @@ -110,6 +110,63 @@ func (w *windowsBadge) SetBadge(label string) error { return w.taskbar.SetOverlayIcon(hwnd, hicon, nil) } +// SetCustomBadge sets the badge label on the application icon with one-off options. +func (w *windowsBadge) SetCustomBadge(label string, options Options) error { + if w.taskbar == nil { + return nil + } + + app := application.Get() + if app == nil { + return nil + } + + window := app.CurrentWindow() + if window == nil { + return nil + } + + hwnd, err := window.NativeWindowHandle() + if err != nil { + return err + } + + const badgeSize = 32 + + img := image.NewRGBA(image.Rect(0, 0, badgeSize, badgeSize)) + + backgroundColour := options.BackgroundColour + radius := badgeSize / 2 + centerX, centerY := radius, radius + + for y := 0; y < badgeSize; y++ { + for x := 0; x < badgeSize; x++ { + dx := float64(x - centerX) + dy := float64(y - centerY) + + if dx*dx+dy*dy < float64(radius*radius) { + img.Set(x, y, backgroundColour) + } + } + } + + var hicon w32.HICON + if label == "" { + hicon, err = createBadgeIcon(badgeSize, img, options) + if err != nil { + return err + } + } else { + hicon, err = createBadgeIconWithText(w, label, badgeSize, img, options) + if err != nil { + return err + } + } + defer w32.DestroyIcon(hicon) + + return w.taskbar.SetOverlayIcon(hwnd, hicon, nil) +} + // RemoveBadge removes the badge label from the application icon. func (w *windowsBadge) RemoveBadge() error { if w.taskbar == nil { @@ -160,9 +217,33 @@ func (w *windowsBadge) createBadgeIcon() (w32.HICON, error) { return hicon, err } +func createBadgeIcon(badgeSize int, img *image.RGBA, options Options) (w32.HICON, error) { + radius := badgeSize / 2 + centerX, centerY := radius, radius + innerRadius := badgeSize / 5 + + for y := 0; y < badgeSize; y++ { + for x := 0; x < badgeSize; x++ { + dx := float64(x - centerX) + dy := float64(y - centerY) + + if dx*dx+dy*dy < float64(innerRadius*innerRadius) { + img.Set(x, y, options.TextColour) + } + } + } + + var buf bytes.Buffer + if err := png.Encode(&buf, img); err != nil { + return 0, err + } + + hicon, err := w32.CreateSmallHIconFromImage(buf.Bytes()) + return hicon, err +} + // createBadgeIconWithText creates a badge icon with the specified text. func (w *windowsBadge) createBadgeIconWithText(label string) (w32.HICON, error) { - fontPath := w.fontManager.FindFontOrDefault(w.options.FontName) if fontPath == "" { return w.createBadgeIcon() @@ -215,6 +296,59 @@ func (w *windowsBadge) createBadgeIconWithText(label string) (w32.HICON, error) return w32.CreateSmallHIconFromImage(buf.Bytes()) } +func createBadgeIconWithText(w *windowsBadge, label string, badgeSize int, img *image.RGBA, options Options) (w32.HICON, error) { + fontPath := w.fontManager.FindFontOrDefault(options.FontName) + if fontPath == "" { + return createBadgeIcon(badgeSize, img, options) + } + + fontBytes, err := os.ReadFile(fontPath) + if err != nil { + return createBadgeIcon(badgeSize, img, options) + } + + ttf, err := opentype.Parse(fontBytes) + if err != nil { + return createBadgeIcon(badgeSize, img, options) + } + + fontSize := float64(options.FontSize) + if len(label) > 1 { + fontSize = float64(options.SmallFontSize) + } + + // Get DPI of the current screen + screen := w32.GetDesktopWindow() + dpi := w32.GetDpiForWindow(screen) + + face, err := opentype.NewFace(ttf, &opentype.FaceOptions{ + Size: fontSize, + DPI: float64(dpi), + Hinting: font.HintingFull, + }) + if err != nil { + return createBadgeIcon(badgeSize, img, options) + } + defer face.Close() + + d := &font.Drawer{ + Dst: img, + Src: image.NewUniform(options.TextColour), + Face: face, + } + + textWidth := d.MeasureString(label).Ceil() + d.Dot = fixed.P((badgeSize-textWidth)/2, int(float64(badgeSize)/2+fontSize/2)) + d.DrawString(label) + + var buf bytes.Buffer + if err := png.Encode(&buf, img); err != nil { + return 0, err + } + + return w32.CreateSmallHIconFromImage(buf.Bytes()) +} + // createBadge creates a circular badge with the specified background color. func (w *windowsBadge) createBadge() { w.badgeSize = 32