diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 0f19739..d0f7e4a 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,5 +1,13 @@ name: docs +# Every documentation version is a separate Hugo build, published under +# https://go-widgets.github.io/docs// on the gh-pages branch, where +# is the documentation's own minor version +# (params.docs.version in hugo.yaml: v0.2.0 -> 0.2). main is that +# version's source: each push to main replaces /docs//, and +# /docs/latest/ follows the newest. Pull requests build and check, and publish +# nothing. + on: push: branches: [main] @@ -12,40 +20,135 @@ on: permissions: contents: read +env: + HUGO_VERSION: "0.167.0" + # sha256 of hugo_${HUGO_VERSION}_linux-amd64.tar.gz, from the release's checksums.txt. + HUGO_SHA256: "4d84519b9f619e6d4c3fb45a50157abeabeb724f859c60605f44c23def6e1169" + DOCS_URL: "https://go-widgets.github.io/docs" + jobs: - # Build on every pull request as well as on main, so a change to the docs - # or to the toolchain in requirements.txt has to prove it still builds - # before it lands. Without this the workflow ran only on main, which meant - # a dependency bump had no signal at all. build: runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} steps: - name: Checkout uses: actions/checkout@v7 - - - name: Set up Python - uses: actions/setup-python@v7 with: - python-version: "3.14" + # themes/hextra (the tannevaled/hextra fork, pinned to a release tag) + # and branding (go-widgets/brand) are submodules. + submodules: recursive + # The whole history: each page's creation and modification come from git. + fetch-depth: 0 - - name: Install MkDocs Material + mike + - name: Install Hugo ${{ env.HUGO_VERSION }} run: | - pip install --upgrade pip - pip install -r requirements.txt + curl -fsSL -o /tmp/hugo.tar.gz \ + "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_${HUGO_VERSION}_linux-amd64.tar.gz" + echo "${HUGO_SHA256} /tmp/hugo.tar.gz" | sha256sum -c - + sudo tar -xzf /tmp/hugo.tar.gz -C /usr/local/bin hugo + hugo version + + # The version label is the documentation's minor version, read from hugo.yaml, + # and it is the last segment of baseURL: the version selector reads it there. + - name: Version + id: version + run: | + docs=$(hugo config --format json | jq -r '.params.docs.version') + version=$(printf '%s\n' "${docs}" | sed -nE 's/^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/\1.\2/p') + if [ -z "${version}" ]; then + echo "::error::params.docs.version in hugo.yaml is \"${docs}\", not vMAJOR.MINOR.PATCH" + exit 1 + fi + echo "version=${version}" >> "$GITHUB_OUTPUT" + echo "docs ${docs}: documentation version ${version}" + + # Each page's creation (first commit, followed across renames) and its + # author, for the page history at its foot (params.page.history). + - name: Page history + run: | + mkdir -p data + sh themes/hextra/scripts/page-history.sh > data/pagehistory.json + + # --panicOnWarning: a deprecated setting or a template warning fails the + # build rather than scrolling by. A broken {{< relref >}} is an error. + - name: Build + env: + HUGO_ENVIRONMENT: production + run: >- + hugo --gc --minify --panicOnWarning --printPathWarnings + --baseURL "${DOCS_URL}/${{ steps.version.outputs.version }}/" + + # Internal links and anchors, offline, with the site laid out under + # /docs// as it is served. + - name: Lay out the site for the link check + run: | + mkdir -p _check/docs + cp -R public "_check/docs/${{ steps.version.outputs.version }}" + + - name: Check links + uses: lycheeverse/lychee-action@v2 + with: + args: >- + --offline --no-progress --include-fragments + --index-files index.html + --root-dir ${{ github.workspace }}/_check + '_check/docs/**/*.html' + fail: true - # --strict turns a broken link or an unknown config key into a failure - # rather than a warning nobody reads. - - name: Build the site (strict) - run: mkdocs build --strict + - uses: actions/upload-artifact@v7 + with: + name: site + path: public + retention-days: 7 + + # The offline check above proves the site's own links and anchors. An + # https:// URL is text to it: this job fetches each one the pages name. + links: + name: every external link resolves + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - name: Check + run: | + set -uo pipefail + # The character class matters: an extractor stopping only at ) and " + # carries a closing > or < into the request. + urls=$(grep -rhoE 'https?://[^)">< [:space:]]+' content/ README.md | sed -E 's/[.,`]+$//' | sort -u) + test -n "$urls" || { echo "::error::no links found; are the pages still there?"; exit 1; } + echo "checking $(printf '%s\n' "$urls" | wc -l) distinct URLs" + fail=0 + while read -r u; do + case "$u" in + # Renders whatever it is asked for; proves nothing. + *img.shields.io*) continue ;; + # Documented placeholders, not hosts. + *example.com*|*example.org*|*example.net*|*localhost*|*127.0.0.1*|*.invalid*) continue ;; + esac + # Three attempts: one is a flake detector rather than a link + # checker, and a lane that fails at random gets ignored. + code=000 + for _ in 1 2 3; do + code=$(curl -sS -o /dev/null -w '%{http_code}' -L --max-time 25 "$u" 2>/dev/null) || true + code=${code:-000} + case "$code" in 2*|3*) break ;; esac + sleep 3 + done + case "$code" in + 2*|3*) printf ' ok %s %s\n' "$code" "$u" ;; + *) printf ' FAIL %s %s\n' "$code" "$u"; fail=1 ;; + esac + done <<< "$urls" + test "$fail" -eq 0 || { echo "::error::at least one link does not resolve"; exit 1; } deploy: # Only main publishes. A pull request stops after build above: it has no # business pushing to gh-pages. - if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' + if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' needs: build - # Never two deploys at once: mike pushes gh-pages and the second is - # rejected outright. cancel-in-progress stays false -- killing a publish - # half way through is worse than making it wait. + # Never two deploys at once: both would push gh-pages and the second would + # be rejected. cancel-in-progress stays false -- killing a publish half way + # through is worse than making it wait. concurrency: group: gh-pages-deploy cancel-in-progress: false @@ -53,32 +156,35 @@ jobs: permissions: contents: write steps: - - name: Checkout - uses: actions/checkout@v7 + # The publishing script, from the commit being published. + - uses: actions/checkout@v7 with: - fetch-depth: 0 + path: source - - name: Set up Python - uses: actions/setup-python@v7 + # The published site. Only the version being built is replaced; every + # other version (the MkDocs-built 0.1) stays as it is. + - uses: actions/checkout@v7 with: - python-version: "3.14" + ref: gh-pages + path: gh-pages - - name: Install MkDocs Material + mike - run: | - pip install --upgrade pip - pip install -r requirements.txt + - uses: actions/download-artifact@v7 + with: + name: site + path: site - - name: Configure git for mike - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + - name: Publish ${{ needs.build.outputs.version }} + run: source/scripts/publish-version.sh "${{ needs.build.outputs.version }}" site gh-pages - # mike publishes versioned docs to the gh-pages branch; GitHub Pages serves - # that branch at https://go-compressions.github.io/docs/. The version label - # tracks main as "0.1" aliased "latest"; tag-driven versions can be added - # later. /docs/ redirects to the default ("latest"). - - name: Deploy versioned docs + - name: Push gh-pages + working-directory: gh-pages run: | - git fetch origin gh-pages --depth=1 || true - mike deploy --push --update-aliases 0.1 latest - mike set-default --push latest + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add -A + if git diff --cached --quiet; then + echo "nothing changed" + exit 0 + fi + git commit -q -m "Deployed ${GITHUB_SHA::7} to ${{ needs.build.outputs.version }} with Hugo ${HUGO_VERSION}" + git push origin HEAD:gh-pages diff --git a/.gitignore b/.gitignore index 053c435..7cd05fc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ -/site/ -.venv/ -__pycache__/ +/public/ +/resources/ +/_check/ +.hugo_build.lock +.DS_Store + +# Written by themes/hextra/scripts/page-history.sh before each build (CI and local). +/data/pagehistory.json diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..9dd7a79 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "themes/hextra"] + path = themes/hextra + url = https://github.com/tannevaled/hextra.git +[submodule "branding"] + path = branding + url = https://github.com/go-widgets/brand.git diff --git a/README.md b/README.md new file mode 100644 index 0000000..8e4afc7 --- /dev/null +++ b/README.md @@ -0,0 +1,137 @@ +# docs + +[![docs](https://github.com/go-widgets/docs/actions/workflows/docs.yml/badge.svg)](https://github.com/go-widgets/docs/actions/workflows/docs.yml) +[![License](https://img.shields.io/badge/license-BSD--3--Clause-0D9488?style=flat-square)](LICENSE) + +**The go-widgets documentation site**, published at +. + +A [Hugo](https://gohugo.io/) site with the [tannevaled/hextra](https://github.com/tannevaled/hextra) +fork of the [Hextra](https://github.com/imfing/hextra) theme, laid out like the +[go-fileshare documentation](https://github.com/go-fileshare/docs), with +go-widgets' branding. The content is derived from the modules' code, not only +from their READMEs, and where the two disagree the pages follow the code: a +claim here should be traceable to something a module does. + +## Versions + +go-widgets is nineteen modules, each released on its own, so no module's +version can label the site. A version of the documentation is **its own minor +version**: + +| Source | Published under | +| --- | --- | +| branch `main`, with `params.docs.version: v0.2.0` in `hugo.yaml` | `go-widgets.github.io/docs/0.2/` | +| the newest version | also `go-widgets.github.io/docs/latest/` | + +`https://go-widgets.github.io/docs/` redirects to `latest/`. Each push to `main` +replaces the directory of the version it describes, and nothing else. + +A patch (`v0.2.1`) updates `0.2` in place. Raise the minor (`v0.3.0`) when the +pages of the current version should stay readable as they were: `0.3` is then +published beside `0.2`, and `latest` moves to it. + +The version selector in the navbar lists every version from +`docs/versions.json` and opens the same page in the version chosen; if it is +not there, the same page in English, else that version's home. + +Version 0.1 was built with MkDocs and mike before this site moved to Hugo. +Its directory on `gh-pages` is kept as it was published, and it reads the same +`versions.json` (mike's format). + +## Languages + +English is the default and sits at the root of each version +(`/docs//surfaces/terminal/`). French, Spanish and German are under +their code (`/docs//fr/surfaces/terminal/`, `/es/`, `/de/`). Version +0.1 is English only, and the version selector falls back to its English page. +The language switch at the foot of the sidebar opens the same page in another +language, in the same version. + +To add a language ``: + +1. `content//`: a translation of every page, with the same file names + (that is how a page and its translation are paired). A translated heading + keeps the English one's anchor as an explicit id + (`## Comment Open choisit {#how-open-chooses}`), so the relrefs resolve in + every language and links into a page keep their fragment. +2. `i18n/.yaml`: the site's own strings (copyright, version). +3. A `languages.` entry in `hugo.yaml`: label, `contentDir`, weight, + `params.flag`, the translated `params.description`, and a `dateFormat`. +4. Its flag in `static/images/flags/`, from + [lipis/flag-icons](https://github.com/lipis/flag-icons) (4x3, MIT). + +Declare a language only with its content: a declared language without pages +is an empty entry in the switch. A change to an English page is a change to +its three translations too. + +## Layout + +| Path | What | +| --- | --- | +| `content/en/` | the pages; `_index.md` is a section's own page, `weight` orders the sidebar | +| `i18n/.yaml` | the site's strings, beside the theme's | +| `static/images/flags/` | the language switch's flags (lipis/flag-icons, MIT) | +| `hugo.yaml` | `baseURL`, the documentation's version, the languages, the navbar (projects › version › theme › search › GitHub), the brand mounts | +| `layouts/_partials/custom/version-select.html` | the version selector (reads `versions.json`) | +| `layouts/_partials/favicons.html` | the brand's favicons | +| `assets/css/custom.css` | the brand teal as Hextra's primary colour, and the table headers | +| `themes/hextra` | submodule: [tannevaled/hextra](https://github.com/tannevaled/hextra), pinned to a release tag | +| `branding` | submodule: [go-widgets/brand](https://github.com/go-widgets/brand), pinned to a release tag (the mark, its PNGs and ICO, and their LICENSE) | +| `scripts/publish-version.sh` | puts one build into a `gh-pages` checkout and rewrites `versions.json`, `latest` and the root redirect | + +## Writing a page + +Each page starts with a title, a one-sentence description, and tags: + +```yaml +--- +title: "A native window" +linkTitle: "Native window" +weight: 10 +description: "…" +tags: [surfaces, window] +--- +``` + +Link to other pages with `{{< relref "/surfaces/native-window.md#how-open-chooses" >}}`: +a broken reference fails the build, and a missing anchor fails the link check. +A heading's anchor is a URL other sites link to: when a heading's words +change, keep the old anchor with an explicit `{#id}`. + +A number or a version written in a page is measured at a tag, and the page +says which: a README read at a later tag than it was written at is how the +landing page came to say toolkit v0.109.0 while the tag was v0.328.0. + +The page history at the foot of each page (created, modified, by whom) comes +from git: `themes/hextra/scripts/page-history.sh` writes +`data/pagehistory.json` before the build. + +## Working locally + +```sh +git clone --recurse-submodules https://github.com/go-widgets/docs.git +cd docs +mkdir -p data && sh themes/hextra/scripts/page-history.sh > data/pagehistory.json +hugo server --baseURL http://localhost:1313/docs/0.2/ +``` + +then open . Without a `versions.json` the +selector shows only the current version. Hugo 0.146 or later (CI uses the +version pinned in the workflow); no Python, no Node. + +## Publication + +`.github/workflows/docs.yml`: + +| Job | When | What | +| --- | --- | --- | +| `build` | pull requests, `main` | builds with `--baseURL …/docs//`, checks internal links and anchors (lychee, offline), uploads the site | +| `links` | pull requests, `main` | fetches every external URL the pages name | +| `deploy` | `main` | `scripts/publish-version.sh` into `gh-pages`, then a commit and a push; one deploy at a time | + +GitHub Pages serves the `gh-pages` branch. + +## Licence + +BSD-3-Clause. diff --git a/assets/css/custom.css b/assets/css/custom.css new file mode 100644 index 0000000..49b97ca --- /dev/null +++ b/assets/css/custom.css @@ -0,0 +1,30 @@ +/* go-widgets branding on Hextra (github.com/go-widgets/brand). + * + * The teal of the org mark (#0D9488) and of the landing page, which uses + * #14B8A6 as its accent in the dark theme. Hextra derives its whole primary + * scale (links, active sidebar entry, buttons) from these three variables. + * + * Light theme: #0D9488 = hsl(175 84% 32%), the mark's own colour. + * Dark theme: #14B8A6 = hsl(173 80% 40%), the landing page's dark accent. */ +:root { + --primary-hue: 175deg; + --primary-saturation: 84%; + --primary-lightness: 32%; +} +.dark { + --primary-hue: 173deg; + --primary-saturation: 80%; + --primary-lightness: 40%; +} + +/* Tables carry much of these pages -- which back-end does what, which + * binder owns which field -- so their headers stand out in the brand colour, + * a size smaller than the prose. The header is a shade deeper than the mark + * (#0F766E): white on #0D9488 is 3.7:1, under the 4.5:1 small text needs; + * on #0F766E it is 5.5:1. The same shade serves both themes. */ +.content table { font-size: 0.875em; } +.content table th { background: #0F766E; color: #fff; } + +/* The version selector's list hangs from its button's right edge, so that it + * stays on screen at the end of the navbar. */ +.fs-version-select .hextra-nav-menu-items { left: auto !important; right: 0; transform: none !important; } diff --git a/branding b/branding new file mode 160000 index 0000000..1e8f4c8 --- /dev/null +++ b/branding @@ -0,0 +1 @@ +Subproject commit 1e8f4c865a99971e0a61ed7a8aee0057a8067517 diff --git a/content/de/_index.md b/content/de/_index.md new file mode 100644 index 0000000..c8d8c0b --- /dev/null +++ b/content/de/_index.md @@ -0,0 +1,76 @@ +--- +title: "go-widgets" +linkTitle: "Startseite" +type: docs +cascade: + type: docs +description: "Ein Widget-Toolkit in reinem Go und jede Oberfläche, auf die es zeichnet: ein natives Fenster unter X11, Wayland, macOS und Windows, ein Browser-Canvas, ein Terminal, eine Android-APK." +--- + +**Ein Widget-Baum, von Go gezeichnet, auf jeder Oberfläche, die Go erreicht.** Ein natives +Fenster unter X11, Wayland, macOS und Windows; ein `` in einem Browser-Tab; ein +Terminal; eine Android-APK. Keine C-Toolchain, keine Webview, kein System-Widget-Satz: +Jedes Modul baut mit `CGO_ENABLED=0`, und jedes Pixel zeichnet das +Toolkit. + +```go +w, err := window.Open(window.Config{Title: "Hello"}) +if err != nil { + return err +} +defer w.Close() + +box := toolkit.NewVBox() +box.Append(toolkit.NewLabel("Hello from go-widgets")) +box.Append(toolkit.NewButton("Click me", func() { /* ... */ })) +return w.Run(box) // an X11 or Wayland window, an NSWindow, a Win32 window +``` + +Das ist +[`cmd/windowdemo`](https://github.com/go-widgets/window/blob/main/cmd/windowdemo/main.go), +gekürzt. Dieselbe `box` in einem Browser, einem Terminal oder einem PNG gezeichnet ist derselbe +Code mit einer anderen Oberfläche darunter. + +## Warum ein Toolkit alles zeichnet {#why-one-toolkit-draws-everything} + +Ein Widget, das den plattformeigenen Button aufruft, ist so portabel wie die am wenigsten +fähige Plattform, auf die es zielt, und sieht auf jeder anders aus. go-widgets geht den +umgekehrten Weg: Das `Draw` eines Widgets spricht immer nur mit einem +[`painter.Painter`]({{< relref "/rendering/painter.md" >}}), einer Handvoll +Primitive (ein Rechteck füllen, eines umranden, ein Pixel setzen, Text zeichnen). Ob +aus diesen Aufrufen RGBA-Pixel oder Terminalzellen werden, ist Sache des Back-Ends, +nicht des Widgets. + +Was das kostet, wird offen gesagt: standardmäßig kein natives Aussehen und keine Barrierefreiheit +geschenkt – das Toolkit veröffentlicht seinen eigenen +[Barrierefreiheitsbaum]({{< relref "/rendering/toolkit.md#accessibility" >}}) +an jede Plattform. Was es dafür bekommt, ist ein einziger Widget-Satz, einmal getestet, mit demselben +Verhalten überall, und ein Binary, das sich von jeder Maschine aus für jede dieser +Plattformen cross-kompilieren lässt. + +## Wie es weitergeht {#where-to-go-next} + +| | | +|---|---| +| [Eine erste Anwendung]({{< relref "/getting-started.md" >}}) | aus `app-template` oder aus einem nackten Fenster | +| [Wie ein Widget auf den Bildschirm kommt]({{< relref "/rendering/_index.md" >}}) | die Painter-Schnittstelle, der Widget-Satz, Skins | +| [Die Painter-Schnittstelle]({{< relref "/rendering/painter.md" >}}) | die Primitive, mit denen jedes Widget gezeichnet wird | +| [Das Toolkit]({{< relref "/rendering/toolkit.md" >}}) | 160 Widget-Typen, Themes, Layouts, Text, Barrierefreiheit | +| [Skins]({{< relref "/rendering/skin.md" >}}) | die Teile und Zustände eines Widgets in einer Datendatei | +| [Zustand lebt in einem View-Model]({{< relref "/state/_index.md" >}}) | warum Widget-Felder nicht von Hand zugewiesen werden | +| [MVVM]({{< relref "/state/mvvm.md" >}}) | Observables, Commands, Binder, Rückgängig | +| [Das Daten-Rückgrat]({{< relref "/state/data.md" >}}) | typisierte Datensätze, Abfragen, ein lokaler oder entfernter Speicher | +| [Wohin es zeichnet]({{< relref "/surfaces/_index.md" >}}) | jedes Back-End und wie es ausgewählt wird | +| [Ein natives Fenster]({{< relref "/surfaces/native-window.md" >}}) | X11, Wayland, Cocoa, Win32, GTK4, Android, wasmbox | +| [Eine Anwendung und ihr Tray]({{< relref "/surfaces/application.md" >}}) | Lebenszyklus, Erscheinungsbild, ein Menüleisten-Symbol | +| [Ein Browser-Tab]({{< relref "/surfaces/browser.md" >}}) | ein einfaches `` oder ein wasmdesk-Fenster | +| [Ein Terminal]({{< relref "/surfaces/terminal.md" >}}) | zellennative Widgets und ein interaktiver Runner | +| [Android]({{< relref "/surfaces/android.md" >}}) | eine echte APK, gezeichnet von einem CGO-freien Prozess | +| [Snapshots]({{< relref "/surfaces/snapshots.md" >}}) | ein Widget als SVG oder PNG | +| [Regeln, die die CI durchsetzt]({{< relref "/guard-rails/_index.md" >}}) | `mvvmlint` und `bricolint` | +| [Module]({{< relref "/modules.md" >}}) | alle neunzehn, mit ihren Quellen und Referenzen | +| [Stand]({{< relref "/status.md" >}}) | was wo geprüft ist und was noch fehlt | + +## Lizenz {#licence} + +BSD-3-Clause. diff --git a/content/de/getting-started.md b/content/de/getting-started.md new file mode 100644 index 0000000..9648075 --- /dev/null +++ b/content/de/getting-started.md @@ -0,0 +1,79 @@ +--- +title: "Eine erste Anwendung" +weight: 5 +description: "Mit app-template beginnen, das bereits MVVM-konform und grün ist, oder ein nacktes Fenster öffnen und Widgets hinzufügen." +tags: [erste-schritte, app-template, fenster] +--- + +## Aus dem Template {#from-the-template} + +[`go-widgets/app-template`](https://github.com/go-widgets/app-template) ist ein +Template-Repository: eine kleine, aber echte Browser-Anwendung mit einem Suchfeld, +einem Kategoriefilter, einer Liste der passenden Zeilen und einer Statuszeile. +Alles, was eine größere Anwendung braucht, ist bereits verdrahtet: ein View-Model ohne +Widget darin, eine daran gebundene View, ein wasm-Host, eine 100-%-Coverage-Schranke und die +[MVVM-Schranke]({{< relref "/guard-rails/mvvmlint.md" >}}). + +```sh +git clone https://github.com/go-widgets/app-template my-app +cd my-app +go mod edit -module github.com/you/my-app +./build.sh # dist/app.wasm, dist/wasm_exec.js, dist/index.html +``` + +Stellen Sie `dist/` über HTTP bereit: Eine über `file://` geöffnete Seite kann kein wasm instanziieren. + +| Datei | Was sie enthält | +|---|---| +| `viewmodel.go` | jedes Stück Zustand, als [`mvvm`]({{< relref "/state/mvvm.md" >}})-Observables und -Commands. Kein Widget. Ohne Canvas testbar. | +| `scene.go` | die Widgets, jedes über `mvvmtk` an das View-Model gebunden. Es weist nie selbst ein Zustandsfeld eines Widgets zu. | +| `main.go` | die einzige Datei mit einem Build-Tag (`js && wasm`): leitet Eingaben an das Toolkit weiter und überträgt den Frame per Blit in ein ``. | + +Bearbeiten Sie die ersten beiden und lassen Sie den Zustand weiter durch die Binder fließen: Der +`mvvm`-Job in der CI lässt einen Pull Request scheitern, der ein Feld eines Widgets direkt zuweist. + +## Aus einem nackten Fenster {#from-a-bare-window} + +```go +package main + +import ( + "github.com/go-widgets/toolkit" + "github.com/go-widgets/window" +) + +func main() { + w, err := window.Open(window.Config{Title: "Demo"}) + if err == window.ErrUnsupported { + return // no native back-end on this platform + } + if err != nil { + panic(err) + } + defer w.Close() + + box := toolkit.NewVBox() + box.Append(toolkit.NewLabel("Hello")) + box.Append(toolkit.NewButton("Click me", func() {})) + w.Run(box) // layout, draw, present, dispatch input, until closed +} +``` + +Breite und Höhe auf null belassen bittet das Back-End um einen lesbaren Standardwert; unter +macOS ist das ein Bruchteil des Hauptbildschirms. `CGO_ENABLED=0 go build` erzeugt +das Binary für jede Plattform, von jeder Maschine aus. + +## Welches Modul wofür {#which-module-for-what} + +| Sie wollen | Beginnen Sie mit | +|---|---| +| ein Desktop-Fenster | [`window`]({{< relref "/surfaces/native-window.md" >}}) | +| eine Desktop-Anwendung mit Tray-Symbol und dem Erscheinungsbild des Systems | [`application`]({{< relref "/surfaces/application.md" >}}) | +| eine Seite im Browser | [`webcanvas`]({{< relref "/surfaces/browser.md" >}}) oder das Template oben | +| ein Terminalprogramm | [`tui`]({{< relref "/surfaces/terminal.md" >}}) | +| eine Android-App | [`android`]({{< relref "/surfaces/android.md" >}}) | +| ein Bild eines Widgets für ein README | [`svg`]({{< relref "/surfaces/snapshots.md" >}}) | + +Um den Widget-Satz zu sehen, bevor Sie etwas schreiben, öffnen Sie die +[Galerie](https://go-widgets.github.io/gallery/): jede Widget-Familie, live, +in einem Browser-Canvas. diff --git a/content/de/guard-rails/_index.md b/content/de/guard-rails/_index.md new file mode 100644 index 0000000..da9b459 --- /dev/null +++ b/content/de/guard-rails/_index.md @@ -0,0 +1,24 @@ +--- +title: "Regeln, die die CI durchsetzt" +linkTitle: "CI-Regeln" +weight: 40 +description: "Zwei go/analysis-Prüfungen, die eine Anwendung als verpflichtende Statusprüfungen ausführt: Zustand über MVVM und keine handgezeichnete UI." +tags: [ci, lint] +--- + +Zwei Konventionen halten die UI einer Anwendung ehrlich, und beide würden Abkürzung für +Abkürzung erodieren, wenn sie nur aufgeschrieben wären. Deshalb ist jede ein +[`go/analysis`](https://pkg.go.dev/golang.org/x/tools/go/analysis)-Analyzer, +der über `go vet` läuft, mit einem wiederverwendbaren Workflow, den eine Anwendung als +**verpflichtende** Prüfung markiert. + +| | Lehnt ab | +|---|---| +| [`mvvmlint`]({{< relref "/guard-rails/mvvmlint.md" >}}) | das Zuweisen eines Zustandsfelds eines Widgets von Hand und ein Paket mit Widgets, aber ohne View-Model | +| [`bricolint`]({{< relref "/guard-rails/bricolint.md" >}}) | Painter-Primitive im Anwendungscode und ein Widget, das in jedem Frame neu gebaut wird | + +Beide sind von Grund auf konservativ: Eine Regel schlägt nur an, wenn der **statische +Typ** des Empfängers über `go/types` zu einem Toolkit- oder Painter-Typ aufgelöst wird. +Ein gleichnamiges Feld oder eine gleichnamige Methode eines fremden Typs wird nie gemeldet. Die +beabsichtigten Ausnahmen sind explizite Direktiven im Code, sodass jede dort +aufgeschrieben ist, wo sie gemacht wird. diff --git a/content/de/guard-rails/bricolint.md b/content/de/guard-rails/bricolint.md new file mode 100644 index 0000000..a4379ff --- /dev/null +++ b/content/de/guard-rails/bricolint.md @@ -0,0 +1,53 @@ +--- +title: "bricolint: keine handgezeichnete UI" +linkTitle: "bricolint" +weight: 20 +description: "Lässt einen Pull Request scheitern, der im Anwendungscode Bedienelemente mit Painter-Primitiven zeichnet oder ein Widget in jedem Frame neu baut." +tags: [ci, lint, painter] +--- + +*Bricolage* – improvisiertes, selbstgestricktes Zeichnen – ist der Weg, auf dem eine Oberfläche still und leise +ihr Feedback für Drücken, Hover und Fokus verliert, ihr Theming, ihre HiDPI-Skalierung +und ihre Barrierefreiheit. [`go-widgets/bricolint`](https://github.com/go-widgets/bricolint) +verhindert, dass eine Anwendung, die auf das Toolkit umgestiegen ist, wieder zurückdriftet. + +```go +p.FillRect(bar, theme.Bg) // flagged: hand-drawn chrome +toolkit.NewBackdrop(theme.Bg).Draw(p) // a toolkit widget + +func (v *view) Draw(p painter.Painter) { + b := toolkit.NewButton("ok") // flagged: a new widget every frame + b.Draw(p) +} +``` + +1. **Ein Painter-Primitiv im Anwendungscode**: eine Zeichenmethode (`FillRect`, + `StrokeRect`, `FillPath`, `DrawImage`, `Text`, `PutPixel`, …), aufgerufen auf einem + Empfänger, dessen statischer Typ ein Painter-Typ ist. Die Oberfläche abzufragen oder zu beschneiden + (`Size`, `PushClip`, `PushTranslate`) ist kein Zeichnen und wird nicht + gemeldet. `-primitives=` ersetzt die Liste. +2. **Ein Widget, das in jedem Frame weggeworfen wird**: ein `toolkit.New…`-Aufruf in einer Methode + namens `Draw`, `Paint` oder `Render`. Ein bei jedem Zeichnen neu gebautes Widget behält keinen + Interaktionszustand. Bauen Sie es einmal, halten Sie es in einem Feld, steuern Sie es über eine + Bindung. `-checkthrow=false` schaltet das ab. + +Mancher Code ist ein echtes Blatt – ein Spiel-Framebuffer, ein Painter-Back-End, die +Interna des Toolkits selbst – und sagt das explizit: + +```go +p.FillRect(bg, c) //bricolint:allow engine SVG raster blit — a genuine leaf +``` + +```go +//bricolint:allowfile painter back-end — this file IS the leaf +package pdfsurface +``` + +**Die Begründung ist Pflicht**: Eine Direktive ohne Begründung wird ignoriert, und der +Befund schlägt weiter an, bis jemand die Rechtfertigung aufschreibt. + +```yaml +jobs: + bricolint: + uses: go-widgets/bricolint/.github/workflows/bricolint.yml@main +``` diff --git a/content/de/guard-rails/mvvmlint.md b/content/de/guard-rails/mvvmlint.md new file mode 100644 index 0000000..532c06d --- /dev/null +++ b/content/de/guard-rails/mvvmlint.md @@ -0,0 +1,44 @@ +--- +title: "mvvmlint: Zustand geht über das View-Model" +linkTitle: "mvvmlint" +weight: 10 +description: "Lässt einen Pull Request scheitern, der ein Zustandsfeld eines Toolkit-Widgets direkt zuweist oder Widgets, aber kein View-Model hat." +tags: [ci, lint, mvvm] +--- + +[`go-widgets/mvvmlint`](https://github.com/go-widgets/mvvmlint) schlägt nur in +Paketen an, die das Toolkit importieren. + +1. **Ein von Hand zugewiesenes Zustandsfeld eines Widgets.** `entry.Text = "x"`, wobei der + statische Typ von `entry` ein Toolkit-Widget und `Text` ein Zustandsfeld ist, wird + gemeldet. `&entry.Text` – was die [Binder]({{< relref "/state/mvvm.md" >}}) + entgegennehmen – ist keine Zuweisung und wird nie gemeldet; ebenso wenig ein zusammengesetztes + Literal (`toolkit.Entry{Text: "x"}`) oder das Zusammensetzen eines Callbacks wie + `OnChange`. +2. **Widgets und kein View-Model.** Ein Paket, das das Toolkit importiert, aber nicht + `go-widgets/mvvm`, erhält eine Diagnose. `-requirevm=false` schaltet das für + eine Widget-Bibliothek ab, die berechtigterweise kein View-Model hat. + +Die Zustandsfelder sind die der Toolkit-Widgets (`Text`, `Items`, `Rows`, +`Root`, `Selected`, `Value`, `Checked`, …); `-statefields=` ersetzt die Liste. +Eine Datei namens `*_binding.go` oder eine, die `//mvvmlint:allow` enthält, ist von +der ersten Regel ausgenommen. + +```sh +go install github.com/go-widgets/mvvmlint/cmd/mvvmlint@latest +go vet -vettool="$(go env GOPATH)/bin/mvvmlint" ./... +``` + +```yaml +# .github/workflows/mvvm.yml +name: mvvm +on: + pull_request: + push: + branches: [main] +jobs: + mvvm: + uses: go-widgets/mvvmlint/.github/workflows/mvvmlint.yml@main +``` + +[`app-template`]({{< relref "/getting-started.md" >}}) wird mit diesem Job ausgeliefert. diff --git a/content/de/modules.md b/content/de/modules.md new file mode 100644 index 0000000..3987fc1 --- /dev/null +++ b/content/de/modules.md @@ -0,0 +1,46 @@ +--- +title: "Module" +weight: 50 +description: "Die neunzehn Module von go-widgets, wofür jedes da ist und wo seine Quellen und seine API-Referenz liegen." +tags: [module] +--- + +Die API-Referenz erzeugt +[pkg.go.dev](https://pkg.go.dev/) aus den Quellen und folgt dabei den veröffentlichten Tags; diese +Seiten erklären sie, statt sie zu kopieren. Der Tag ist der neueste zum Zeitpunkt des +Schreibens – maßgeblich ist die Tag-Liste des jeweiligen Repositorys. + +| Modul | Tag | Was es ist | Referenz | +|---|---|---|---| +| [`painter`](https://github.com/go-widgets/painter) | v0.15.0 | [die Zeichenschnittstelle]({{< relref "/rendering/painter.md" >}}): Pixel- und Zellen-Painter | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/painter) | +| [`toolkit`](https://github.com/go-widgets/toolkit) | v0.328.0 | [der Widget-Satz]({{< relref "/rendering/toolkit.md" >}}), Themes, Layouts, Text | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/toolkit) | +| [`skin`](https://github.com/go-widgets/skin) | v0.2.0 | [Widgets als Daten beschrieben]({{< relref "/rendering/skin.md" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/skin) | +| [`isoicons`](https://github.com/go-widgets/isoicons) | v0.3.0 | isometrische Icon-Pakete (Cloud-native, AWS) für die isometrischen Diagramme des Toolkits | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/isoicons) | +| [`mvvm`](https://github.com/go-widgets/mvvm) | v0.13.0 | [Observables, Commands, Binder, Rückgängig]({{< relref "/state/mvvm.md" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/mvvm) | +| [`mvvmtk`](https://github.com/go-widgets/mvvmtk) | v0.14.1 | [Binder mit einem Aufruf für Toolkit-Widgets]({{< relref "/state/mvvm.md#mvvmtk-one-call-per-widget" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/mvvmtk) | +| [`data`](https://github.com/go-widgets/data) | v0.3.0 | [typisierte Datensätze, Abfragen, ein lokaler oder entfernter Speicher]({{< relref "/state/data.md" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/data) | +| [`window`](https://github.com/go-widgets/window) | v0.86.1 | [ein natives Fenster]({{< relref "/surfaces/native-window.md" >}}) auf sieben Back-Ends | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/window) | +| [`application`](https://github.com/go-widgets/application) | v0.7.0 | [der Anwendungslebenszyklus]({{< relref "/surfaces/application.md" >}}) oberhalb eines Fensters | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/application) | +| [`tray`](https://github.com/go-widgets/tray) | v0.14.0 | [ein Tray-Symbol und sein Menü]({{< relref "/surfaces/application.md#the-tray" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/tray) | +| [`webcanvas`](https://github.com/go-widgets/webcanvas) | v0.2.0 | [eine Szene in einem Browser-``]({{< relref "/surfaces/browser.md" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/webcanvas) | +| [`tui`](https://github.com/go-widgets/tui) | v0.61.0 | [Terminal-Rendering und zellennative Widgets]({{< relref "/surfaces/terminal.md" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/tui) | +| [`android`](https://github.com/go-widgets/android) | v0.15.0 | [eine APK, gezeichnet von einem CGO-freien Prozess]({{< relref "/surfaces/android.md" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/android) | +| [`svg`](https://github.com/go-widgets/svg) | v0.6.0 | [ein Rendering als SVG oder PNG]({{< relref "/surfaces/snapshots.md" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/svg) | +| [`mvvmlint`](https://github.com/go-widgets/mvvmlint) | v0.4.0 | [die MVVM-Prüfung]({{< relref "/guard-rails/mvvmlint.md" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/mvvmlint) | +| [`bricolint`](https://github.com/go-widgets/bricolint) | v0.4.0 | [die Prüfung gegen handgezeichnete UI]({{< relref "/guard-rails/bricolint.md" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/bricolint) | +| [`app-template`](https://github.com/go-widgets/app-template) | v0.4.0 | [der Ausgangspunkt für eine Anwendung]({{< relref "/getting-started.md" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/app-template) | +| [`gallery`](https://github.com/go-widgets/gallery) | v0.6.0 | die [Live-Demo](https://go-widgets.github.io/gallery/) jeder Widget-Familie | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/gallery) | +| [`desktop`](https://github.com/go-widgets/desktop) | v0.19.0 | eine Desktop-Shell, die go-freedesktop und go-widgets kombiniert, nativ und in wasmdesk | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/desktop) | + +## Woran sich jedes Modul halten muss {#what-every-module-is-held-to} + +- `CGO_ENABLED=0`: kein cgo und kein Aufruf eines Kommandozeilenwerkzeugs anstelle + einer Bibliothek. +- Cross-kompiliert für amd64, arm64, riscv64, loong64, ppc64le und s390x – letzteres + Big-Endian, was jede Wire-Kodierung ehrlich hält – und, wo es zutrifft, + `js/wasm`. Zwölf der neunzehn schaffen heute alle sechs, zwei sind + Browser-Anwendungen, und die übrigen fünf sind unter [Stand]({{< relref "/status.md#what-ci-does-not-cover-yet" >}}) aufgeführt. +- Eine Schranke für die Anweisungsabdeckung in der CI: 100 % bei achtzehn der neunzehn. Wo eine + Datei nicht in einem Test laufen kann (eine Browser-Schleife, eine native Run-Loop), sagt das Modul, + welche Datei ausgenommen ist und warum. Die Ausnahmen stehen unter [Stand]({{< relref "/status.md" >}}). +- BSD-3-Clause. diff --git a/content/de/rendering/_index.md b/content/de/rendering/_index.md new file mode 100644 index 0000000..bf7fb76 --- /dev/null +++ b/content/de/rendering/_index.md @@ -0,0 +1,29 @@ +--- +title: "Wie ein Widget auf den Bildschirm kommt" +linkTitle: "Rendering" +weight: 10 +description: "Ein Widget zeichnet über einen Painter; der Painter entscheidet, ob daraus Pixel oder Terminalzellen werden; ein Back-End stellt das Ergebnis dar." +tags: [rendering, painter, toolkit] +--- + +Drei Schichten, und jede kennt nur die unter ihr: + +```text +toolkit widget tree Button, Table, Agenda, VBox ... (or a skin.Object) + │ Draw(p painter.Painter, theme *Theme) + ▼ +painter.Painter FillRect, StrokeRect, Text, PutPixel, ... + │ + ├─ PixelPainter → an RGBA []byte → window, browser canvas, PNG, Android + └─ CellPainter → a grid of cells → a terminal, as 24-bit ANSI +``` + +Ein Widget erfährt nie, welchen Painter es bekommen hat. Ein Back-End erfährt nie, +welche Widgets gezeichnet haben. Genau deshalb kann ein Widget-Baum in einem +nativen Fenster, einem Browser und einem Terminal ohne eine Zeile bedingten Codes angezeigt werden. + +| | | +|---|---| +| [Die Painter-Schnittstelle]({{< relref "/rendering/painter.md" >}}) | die Primitive, die optionalen Fähigkeiten, die beiden Painter | +| [Das Toolkit]({{< relref "/rendering/toolkit.md" >}}) | der Widget-Satz, Themes, Layouts, Text und Barrierefreiheit | +| [Skins]({{< relref "/rendering/skin.md" >}}) | die Teile und Zustände eines Widgets als Daten statt in Go beschreiben | diff --git a/content/de/rendering/painter.md b/content/de/rendering/painter.md new file mode 100644 index 0000000..d361134 --- /dev/null +++ b/content/de/rendering/painter.md @@ -0,0 +1,71 @@ +--- +title: "Die Painter-Schnittstelle" +linkTitle: "Painter" +weight: 10 +description: "Die Primitive, mit denen jedes Widget gezeichnet wird, die optionalen Fähigkeiten, die ein Painter hinzufügen kann, und die Pixel- und Zellen-Painter." +tags: [rendering, painter] +--- + +[`go-widgets/painter`](https://github.com/go-widgets/painter) definiert die eine +Schnittstelle, über die jedes Widget zeichnet: + +```go +type Painter interface { + FillRect(r Rect, c RGBA) + StrokeRect(r Rect, c RGBA, lineW int) + FillRoundRect(r Rect, radius int, c RGBA) + StrokeRoundRect(r Rect, radius int, c RGBA, lineW int) + PutPixel(x, y int, c RGBA) + Text(x, y int, s string, ink RGBA) + Size() (w, h int) +} +``` + +Der Satz ist absichtlich klein: Jedes Back-End muss ihn vollständig implementieren, und ein +Terminalraster kann keine Ecke abrunden und keine Strichbreite variieren. Deshalb legt der Vertrag +fest, was geschieht, wenn es das nicht kann – die Breite bei `StrokeRect` ist ein Hinweis, den ein Zellenraster +ignoriert, und `FillRoundRect` fällt dort auf eine eckige Füllung zurück. + +## Optionale Fähigkeiten {#optional-capabilities} + +Was nicht jede Oberfläche kann, steht nicht in der Basisschnittstelle. Ein Widget, das +es braucht, macht eine Typzusicherung und zeichnet etwas Sinnvolles, wenn die Antwort Nein lautet: + +| Schnittstelle | Methoden | Wofür | +|---|---|---| +| `Clipper` | `PushClip`, `PopClip` | ein gescrolltes Kind innerhalb der Grenzen seines Elternelements halten | +| `Translator` | `PushTranslate`, `PopTranslate` | ein Kind in seinen eigenen Koordinaten zeichnen | +| `PathPainter` | `FillPath`, `StrokePath` | Kurven, Diagramme, kantengeglättete Formen | +| `ImagePainter` | `DrawImage` | Icons, Vorschaubilder, Bilder | +| `MaskPainter` | `DrawMask` | Glyphen und andere Deckungsmasken in einer Farbe | +| `FacePainter` | `TextFace` | Text in einer bestimmten Schrift und Größe | + +```go +if c, ok := p.(painter.Clipper); ok { + c.PushClip(bounds) + defer c.PopClip() +} +``` + +## Die beiden Painter {#the-two-painters} + +| | Schreibt in | Dargestellt von | +|---|---|---| +| `NewPixelPainter(buf, w, h)` | einen RGBA-`[]byte`, der dem Aufrufer gehört (`NewPixelPainterBGRA` für BGRA-Oberflächen) | einem nativen Fenster, einem Browser-Canvas, einer Android-Oberfläche, einem PNG oder SVG | +| `NewCellPainter(w, h)` | ein Zellenraster (Rune, Vordergrund, Hintergrund) mit einem 24-Bit-ANSI-Serialisierer | einem Terminal, über [`tui`]({{< relref "/surfaces/terminal.md" >}}) | + +Der Puffer bleibt im Besitz des Aufrufers. Ein `PixelPainter` übersetzt nur Primitive +in Schreibzugriffe, sodass derselbe `[]byte` an ein Fenster-Back-End, ein +`` oder einen Bild-Encoder übergeben werden kann, ohne dazwischen zu kopieren. + +## Ausprobieren {#trying-it} + +Die Demos des Repositorys zeichnen dieselben drei Widgets über beide Painter: + +```sh +go run ./cmd/wui-demo --out demo.png # pixels, written as a PNG +go run ./cmd/tui-demo --theme dark # cells, written as ANSI to stdout +``` + +und [go-widgets.github.io/painter](https://go-widgets.github.io/painter/) zeigt +den Pixel-Painter in einem Browser. diff --git a/content/de/rendering/skin.md b/content/de/rendering/skin.md new file mode 100644 index 0000000..6b36282 --- /dev/null +++ b/content/de/rendering/skin.md @@ -0,0 +1,81 @@ +--- +title: "Skins: die Teile und Zustände eines Widgets als Daten" +linkTitle: "Skins" +weight: 30 +description: "go-widgets/skin beschreibt die Teile eines Widgets, seine Darstellung pro Zustand und signalgesteuerte Übergänge in einer .skin.json-Datei, gerendert von einer einzigen Engine." +tags: [rendering, skin, theme] +--- + +Die Widgets des Toolkits sind in Go geschrieben: Die Teile eines `Button`, sein Aussehen bei Hover, +gedrückt und deaktiviert sowie sein Klick-Feedback liegen alle in `button.go`. +Das ist schnell und exakt, und jede visuelle Änderung ist eine Codeänderung. + +[`go-widgets/skin`](https://github.com/go-widgets/skin) verlagert diese Beschreibung +in Daten – die Idee von Edje aus Enlightenment. Ein `.skin.json`-Dokument benennt die +**Teile** (parts) eines Widgets, ihr Aussehen in jedem **Zustand** (state) und die **Programme** (programs), die +sie zwischen Zuständen bewegen, wenn ein Signal eintrifft. Ein neuer Skin oder ein neues +Bedienelement braucht dann keine Neukompilierung: Das Programm lädt andere Bytes. + +```go +theme, err := skin.Load(jsonBytes) // parse and validate every collection +if err != nil { + return err +} +obj, _ := theme.New("button") +obj.Bind(skin.NewMVVMContext().Set("$.label", labelObs)) +obj.SetBounds(toolkit.Rect{W: 96, H: 28}) + +// each frame +obj.OnEvent(ev) // pointer events become signals, signals states +busy := obj.Tick(time.Now()) // advance the transitions +obj.Draw(p, theme.Palette()) +``` + +Ein `skin.Object` ist ein `toolkit.Widget` und sitzt deshalb in einer `VBox` neben +handgeschriebenen Widgets. Die Layouts platzieren Widgets; ein Skin ordnet an, was +in einem Widget liegt. + +## Das Format {#the-format} + +```jsonc +{ + "collections": { + "button": { + "min": { "w": 96, "h": 28 }, + "parts": [ + { "name": "bg", "type": "rect", + "states": { + "default": { "color": "@surface", "border": "@border", "radius": 6 }, + "hover": { "color": "@surface_alt", "border": "@border", "radius": 6 }, + "pressed": { "color": "@accent", "border": "@border", "radius": 6 } } }, + { "name": "label", "type": "text", "text_from": "$.label", "align": [0.5, 0.5], + "states": { "default": { "ink": "@on_surface" } } } + ], + "programs": [ + { "on": "mouse,in", "target": ["bg", "label"], "to": "hover", "in": 0.12, "ease": "ease_out_cubic" }, + { "on": "mouse,click", "emit": "clicked" } + ] + } + } +} +``` + +- Ein **Teil** ist ein `rect`, `text` oder `image`, platziert über zwei relative Ecken + (`rel1`, `rel2`) gegenüber dem Objekt oder einem früheren Teil, mit Pixel- und + schriftrelativen (`offset_em`) Versätzen, sodass ein Textteil der Schrift folgt. +- Ein **Zustand** setzt Farben, Rahmen, Radius und Sichtbarkeit und kann den Teil verschieben oder + in der Größe ändern. +- Ein **Programm** reagiert auf ein Signal, bewegt Teile über eine Dauer hinweg mit einer benannten + Easing-Funktion in einen Zustand und kann ein Signal nach außen senden (`emit`) – zum Beispiel an ein + [`mvvm.Command`]({{< relref "/state/mvvm.md" >}}). +- Farben sind `@tokens`, aufgelöst gegen das `Theme` des Toolkits (einschließlich jedes + GTK-`@define-color`-Eintrags als `@extra:`), Hex-Werte oder RGBA-Arrays. + +JSON wird von der Standardbibliothek dekodiert: keine Abhängigkeit von Dritten. + +## Gegen die handgeschriebenen Widgets nachgewiesen {#proven-against-the-hand-written-widgets} + +Button, Checkbox, Switch, Chip und Card des Toolkits sind jeweils noch einmal +als Skin-Collection geschrieben, und die Tests prüfen, dass beide in jedem Zustand **pixelgenau +identisch** rendern. Ein Benchmark misst ein Skin-`Draw` gegen das +handgeschriebene. diff --git a/content/de/rendering/toolkit.md b/content/de/rendering/toolkit.md new file mode 100644 index 0000000..a9cfe78 --- /dev/null +++ b/content/de/rendering/toolkit.md @@ -0,0 +1,74 @@ +--- +title: "Das Toolkit" +linkTitle: "Toolkit" +weight: 20 +description: "160 Widget-Typen, gezeichnet über die Painter-Schnittstelle: Themes, Layouts, Text, Eingabe, Barrierefreiheit." +tags: [rendering, toolkit, widgets, theme, barrierefreiheit] +--- + +[`go-widgets/toolkit`](https://github.com/go-widgets/toolkit) ist der Widget-Satz. +Jedes Widget implementiert dieselbe Schnittstelle – `Bounds`, `SetBounds`, +`Draw(p painter.Painter, theme *Theme)`, `HitTest`, `OnEvent` –, sodass ein Host +einen Button, eine Tabelle und einen Terminkalender gleich behandelt. Nur Standardbibliothek, +`CGO_ENABLED=0`, 100 % Anweisungsabdeckung. + +In v0.328.0 hat es **160 exportierte Widget-Typen** in etwa **69.000 Zeilen** +Widget-Code. Beide Zahlen sind an diesem Tag gemessen, nicht übernommen: Das +README des Toolkits nennt die Befehle, und man sollte sie erneut ausführen, statt +die Zahlen zu wiederholen. + +## Die Familien {#the-families} + +| Familie | Widgets | +|---|---| +| Aktion | `Button`, `ToggleButton`, `CheckButton`, `RadioButton` + `RadioGroup`, `Switch`, `SplitButton`, `IconButton`, `CycleButton`, `Chip`, `SegmentedBar` | +| Eingabe | `Entry`, `TextView` (Auswahl, IME-Vorschau, Syntax-Spans, Zeilennummern), `SpinButton`, `Scale`, `RangeSlider`, `SearchEntry`, `TagField`, `ComboBox`, `FormField` mit `Validate`/`Rule` | +| Auswahl | `ListBox`, `TreeView`, `DropDown` | +| Container | `Container` mit austauschbarem `Layout` (`FitLayout`, `BoxLayout`, `BorderLayout`, `CardLayout`, `FlowLayout`); `HBox`, `VBox`, `Grid`, `Frame`, `Dock`, `Border`, `Stack`, `Overlay`, `Paned`, `Expander`, `Accordion`; ein deklarativer `Node`-Builder | +| Navigation | `Menu`, `MenuBar` (mit `Alt`+Buchstabe), `ContextMenu`, `Popover`, `CommandPalette`, `Dialog`, `MessageDialog`, `Wizard`, `Notebook`, `ViewSwitcher`, `Carousel` | +| Rückmeldung | `ProgressBar`, `ProgressCircle`, `LevelBar`, `Spinner`, `Tooltip`, `Notification`, `Toast`, `Banner`, `Alert`, `Badge`, `Skeleton` | +| Daten | `Table` (Zellbearbeitung, fixierte Spalten, Gruppenzeilen, Aggregate, aufklappbare Zeilen), `TreeTable`, `PropertyGrid`, `Kanban`, `Gantt`, `Agenda` (Woche, Monat, Quartal, Jahr; mehrere Kalender) | +| Diagramme | `LineChart`, `BarChart`, `PieChart`, `AreaChart`, `ScatterChart`, `RadarChart`, `Gauge`, `Sparkline` | +| Zusammengesetzt | `FileChooser`, `ColorChooser`, `ColorPicker`, `FontChooser`, `Calendar`, `DatePicker`, `DateRangePicker`, `TimePicker`, `MarkdownView`, `MarkdownEditor`, `TerminalView` | +| Shell | `Window` mit clientseitigen Dekorationen, `HeaderBar`, `Toolbar`, `Statusbar`, `StatusIcon`, `StatusArea`, `Wallpaper`, `Thumbnail` | + +Jedes davon, live: die [Galerie](https://go-widgets.github.io/gallery/). + +## Themes {#themes} + +Ein einziger `Theme`-Wert kaskadiert durch den ganzen Baum: Ändert man eine Farbe an der +Wurzel, zeichnet sich jedes Widget damit neu. `DefaultLight()` und `DefaultDark()` +sind eingebaut, und `LoadGTKTheme(css)` liest den `@define-color`-Block jedes +GTK-3- oder libadwaita-Themes – Adwaita, WhiteSur, Solarized – in ein `Theme` ein. + +## Text {#text} + +Der einkompilierte Standard ist eine 5×7-Bitmap-Schrift, damit Pixeltests ihre +Geometrie behalten. Ein Aufruf schaltet die ganze UI auf kantengeglätteten, geshapten Text um: + +```go +toolkit.UseOpenTypeText() // Atkinson Hyperlegible, bundled; js/wasm-safe +``` + +Die Schrift stammt aus [go-opentype/fonts](https://github.com/go-opentype/fonts) +und wird von [go-opentype](https://github.com/go-opentype/opentype) gerastert, +in Go, ohne C-Schriftbibliothek und ohne Suche nach Systemschriften. +`NewTrueTypeFont(ttf, px)` lädt eine andere Schrift; `NewFallbackFont` verkettet +mehrere, etwa um eine CJK- oder arabische Schrift hinzuzufügen; das eigene `Font`-Feld eines Widgets +überschreibt die globale Schrift nur für dieses Widget. + +## Eingabe {#input} + +Maus, Tastatur, IME-Komposition, Touch (`EventTouchStart`/`Move`/`End`, mit +`GestureRecognizer` für Tippen, langes Drücken und Wischen), Drag and Drop +(`DragSource`, `DropTarget`). Mehrfachauswahl von Zeilen in `ListBox`, `Table` und +`TreeView` funktioniert mit der Maus und mit der Tastatur und beachtet ⌘ ebenso +wie Ctrl, weil unter macOS Ctrl-Klick der Sekundärklick ist. + +## Barrierefreiheit {#accessibility} + +Ein Toolkit, das seine eigenen Pixel zeichnet, gibt einem Screenreader nichts zu lesen, +solange es nicht sagt, was es gezeichnet hat. Widgets implementieren `Accessible` – eine Rolle, einen Namen, +einen Wert –, und `CollectA11y` sammelt sie, damit der Host sie veröffentlicht: AT-SPI unter +Linux über [`window`]({{< relref "/surfaces/native-window.md" >}}) und eine +virtuelle View-Hierarchie unter [Android]({{< relref "/surfaces/android.md#accessibility" >}}). diff --git a/content/de/state/_index.md b/content/de/state/_index.md new file mode 100644 index 0000000..daa89de --- /dev/null +++ b/content/de/state/_index.md @@ -0,0 +1,23 @@ +--- +title: "Zustand lebt in einem View-Model" +linkTitle: "Zustand" +weight: 20 +description: "Warum Anwendungszustand in Observables gehalten und an Widgets gebunden wird, statt in Widget-Felder geschrieben zu werden." +tags: [zustand, mvvm] +--- + +Die Felder eines Widgets – der Text eines Eingabefelds, die Einträge einer Liste, die Zeilen einer Tabelle – sind seine +**Sicht** auf den Zustand, nicht der Zustand selbst. Eine Anwendung, die sie von +irgendeiner Stelle aus zuweist, verstreut ihren Zustand über viele Aufrufstellen, und zwei +dieser Aufrufstellen sind sich irgendwann uneinig darüber, was auf dem Bildschirm steht. + +Deshalb lebt der Zustand in einem **View-Model**: einfache Go-Werte, in Observables verpackt, +ohne ein Widget weit und breit, testbar ohne Fenster. Die View bindet jedes Widget +einmal an das View-Model, und von da an hält die Bindung – nicht die Anwendung – +beide synchron, in beide Richtungen. + +| | | +|---|---| +| [MVVM]({{< relref "/state/mvvm.md" >}}) | `Observable`, `Command`, `ObservableList`; Binder für das Toolkit und die Terminal-Widgets; Rückgängig und Wiederherstellen | +| [Das Daten-Rückgrat]({{< relref "/state/data.md" >}}) | typisierte Datensätze mit Validierung, Abfragen und ein Speicher, der dieselbe Pipeline lokal oder über gRPC ausführt | +| [`mvvmlint`]({{< relref "/guard-rails/mvvmlint.md" >}}) | die CI-Prüfung, die einen Pull Request scheitern lässt, der ein Zustandsfeld eines Widgets von Hand zuweist | diff --git a/content/de/state/data.md b/content/de/state/data.md new file mode 100644 index 0000000..1764dae --- /dev/null +++ b/content/de/state/data.md @@ -0,0 +1,45 @@ +--- +title: "Das Daten-Rückgrat" +linkTitle: "Daten" +weight: 20 +description: "go-widgets/data: typisierte Datensätze mit Validierung, eine reine Abfrage-Engine und ein Speicher, der dieselbe Pipeline im Prozess oder über gRPC ausführt, nativ und in wasm." +tags: [zustand, daten, grpc] +--- + +[`go-widgets/data`](https://github.com/go-widgets/data) ist headless: Es +importiert die Standardbibliothek und [`mvvm`]({{< relref "/state/mvvm.md" >}}), +aber kein Widget. + +| Baustein | Was es ist | +|---|---| +| `Value`, `Kind` | ein vergleichbarer typisierter Skalar: String, Int, Float, Bool | +| `Record`, `Schema`, `Field`, `Rule` | eine typisierte Zeile und ihre Validierung | +| `Query` → `Apply` → `View` | die Abfrage-Engine: filtern, sortieren, gruppieren, paginieren, aggregieren | +| `Proxy` | die Back-End-Schnittstelle: `List`, `Query`, `Mutate` | +| `MemoryProxy` | das Back-End im Prozess | +| `grpcproxy.Server`, `grpcproxy.Client` | derselbe Vertrag über gRPC, transportiert über einen WebSocket | +| `Store[R]` | eine typisierte Sammlung, veröffentlicht als `mvvm.ObservableList` | + +## Dieselbe Antwort, lokal oder entfernt {#the-same-answer-local-or-remote} + +`Store` spricht mit einem `Proxy` und weiß nie, mit welchem. `Apply` ist eine reine +Funktion der Datensätze und der Abfrage, sodass Client und Server denselben +Code auf denselben Zeilen ausführen – und ein `MemoryProxy` und ein `grpcproxy.Client` +liefern byte-identische Views. Genau das prüft der Konformitätstest: eine +Batterie von Sortier-, Filter-, Gruppierungs-, Paginierungs- und Aggregationsabfragen über beide +Proxys und über die Engine, jedes Ergebnis kanonisiert, alle drei +Byte für Byte verglichen. + +Der WebSocket-Transport kompiliert nach `js/wasm`, sodass eine Browser-Anwendung +denselben Dienst über denselben Client erreicht. + +```go +store := data.NewStore(mem, codec) // or a grpcproxy.Client: nothing else changes +store.SetQuery(data.Query{Sorts: []data.Sort{{Field: "salary", Desc: true}}, Limit: 20}) +store.Load(ctx) +items := store.Items() // bind this to a view +``` + +`tkbind.BindTable` stellt einen Store hinter eine Toolkit-`Table`: Ein Klick auf die Kopfzeile +sortiert, ein Group-by gruppiert, und eine validierte Zellbearbeitung wird zu einer Mutation über +den Proxy – derselbe Hin- und Rückweg, lokal oder entfernt. diff --git a/content/de/state/mvvm.md b/content/de/state/mvvm.md new file mode 100644 index 0000000..784560c --- /dev/null +++ b/content/de/state/mvvm.md @@ -0,0 +1,95 @@ +--- +title: "MVVM: Observables, Commands, Binder" +linkTitle: "MVVM" +weight: 10 +description: "go-widgets/mvvm, seine Binder für Toolkit und Terminal, die Ein-Aufruf-Helfer von mvvmtk und ein Undo-Stack." +tags: [zustand, mvvm, mvvmtk, rückgängig] +--- + +[`go-widgets/mvvm`](https://github.com/go-widgets/mvvm) besteht nur aus Generics, +ohne Reflection. Sein Kernpaket importiert überhaupt kein Widget-Paket, sodass ein +View-Model das Pixel-[Toolkit]({{< relref "/rendering/toolkit.md" >}}) +und die [Terminal-Widgets]({{< relref "/surfaces/terminal.md" >}}) gleichermaßen steuert. + +| Primitiv | Rolle | +|---|---| +| `Observable[T]` | eine Eigenschaft: `Get`, `Set`, `Subscribe`. Das Setzen eines gleichen Werts bewirkt nichts. | +| `Command` | eine Aktion, mit `CanExecute` und `RaiseCanExecuteChanged` | +| `ObservableList[T]` | eine Sammlung, die jedes Einfügen, Entfernen, Ersetzen, Verschieben und Zurücksetzen meldet | + +## Ein Formular {#a-form} + +```go +type FormVM struct { + Name *mvvm.Observable[string] + Names *mvvm.ObservableList[string] + Save *mvvm.Command +} + +vm := &FormVM{Name: mvvm.NewObservable(""), Names: mvvm.NewObservableList[string]()} +vm.Save = mvvm.NewCommand( + func() { vm.Names.Append(vm.Name.Get()); vm.Name.Set("") }, + func() bool { return vm.Name.Get() != "" }, // CanExecute +) +mvvm.BindCanExecute(vm.Save, vm.Name) // Save greys out while the name is empty +``` + +## Bindung {#binding} + +Die generischen Binder erreichen ein Widget über einen **Zeiger auf sein Wertfeld und +einen Zeiger auf seinen Callback-Platz**: `BindField` (bidirektional), `OneWay`, +`BindCommand`, `BindList`. Ein Widget löst seinen Callback aus seinem Event-Handler aus +und lässt einen direkten Feldschreibzugriff stumm, und `Observable.Set` ignoriert einen gleichen +Wert – so kann eine bidirektionale Bindung ein Echo erzeugen, ohne in eine Rekursion zu geraten. + +Das Toolkit stellt seine Widgets nach und nach darauf um, ihren Zustand **als** +Observable bereitzustellen (`Label.Text()` gibt ein `*mvvm.Observable[string]` zurück). Dann +gibt es kein Feld mehr vorzubelegen und keinen Callback zusammenzusetzen, nur zwei Eigenschaften, die +übereinstimmen müssen: `BindTwoWay(src, dst, invalidate)`. + +Widgets, deren Callback mehrere Argumente nimmt, bekommen einen kleinen benannten Adapter in einem +Paket pro Back-End, den einzigen Paketen, die ein Back-End importieren: + +| Paket | Adapter | +|---|---| +| `mvvm/tkbind` | `BindRange` (der `RangeSlider` mit zwei Griffen), `BindContainer` und `BindCardActive` (die Kinder eines Containers und ein Card-Layout aus einer Liste oder einem Index), `BindStore` und `BindTable` (eine `Table` über einem [Daten-Store]({{< relref "/state/data.md" >}}): Sortieren, Gruppieren und validierte Inline-Bearbeitung) | +| `mvvm/tuibind` | `BindDropdown`, `BindTableSelection` | + +## mvvmtk: ein Aufruf pro Widget {#mvvmtk-one-call-per-widget} + +[`go-widgets/mvvmtk`](https://github.com/go-widgets/mvvmtk) setzt die echten Feld- und +Callback-Namen jedes Toolkit-Widgets ein, sodass die View sie nie nennen muss: + +```go +unbind := mvvmtk.BindText(search, vm.Query, win.Invalidate) // search.Text ⇄ vm.Query +defer unbind() +``` + +| Helfer | Richtung | +|---|---| +| `BindText` (`SearchEntry`), `BindEntryText`, `BindChecked`, `BindSelectedIndex`, `BindSpin`, `BindListSelection`, `BindViewSwitcher` | bidirektional | +| `BindLabel`, `BindProgress` | View-Model → Widget | +| `BindListItems`, `BindDropDownOptions`, `BindViews`, `BindTree` | Liste → Widget | +| `BindCommand` | der Klick eines Buttons, sein `Disabled`-Zustand und sein Ausgrauen folgen dem Command | + +Jeder Helfer gibt ein `unbind` zurück, das die Bindung löst und den +zuvor vorhandenen Callback wiederherstellt. Ein Button, der an ein Command gebunden ist, das nicht ausgeführt werden kann, +nimmt keinen Klick, kein Enter oder Leerzeichen und keinen Tastaturfokus an. + +## Rückgängig und Wiederherstellen {#undo-and-redo} + +`mvvm/undo` ist ein Command-Stack, der allein auf den Kern-Primitiven aufbaut, und +funktioniert daher unter jeder View: + +```go +s := undo.New() // unlimited, coalescing on; undo.WithLimit(n), undo.WithCoalescing(false) +s.Push(undo.NewCommand("Write hello", do, undoIt)) +s.Undo() +s.Redo() + +mvvm.BindCommand(s.UndoCommand(), &undoBtn.OnClick, setEnabled) +mvvm.OneWay(s.UndoTextBinding(), &undoBtn.Text, repaint) // "Undo Write hello" +``` + +Eine Folge von Tastenanschlägen wird zu einem Schritt zusammengefasst, und ein Push nach einem Undo verwirft +den Redo-Rest. diff --git a/content/de/status.md b/content/de/status.md new file mode 100644 index 0000000..3ef0181 --- /dev/null +++ b/content/de/status.md @@ -0,0 +1,64 @@ +--- +title: "Stand" +weight: 60 +description: "Was auf einem echten Display, einem echten Gerät oder in einem echten Terminal nachgewiesen wurde; was nur kompiliert wird; und was die CI noch nicht abdeckt." +tags: [stand] +--- + +Diese Seiten wurden mit den Modulen in den unter +[Module]({{< relref "/modules.md" >}}) aufgeführten Tags abgeglichen. Wo das README eines Moduls und sein Code +sich widersprechen, folgen die Seiten dem Code. + +## Live nachgewiesen {#proven-live} + +Ein im Prozess getesteter Codec beweist den Codec. Dies sind die Stellen, an denen +die Plattform selbst gefragt wurde: + +| Was | Wo es nachgewiesen wurde | +|---|---| +| X11-Fenster | CI, unter Xvfb: ein bekanntes Muster dargestellt, aufgenommen und abgetastet; ein Klick und eine Taste mit `xdotool` synthetisiert und das ausgelöste `toolkit.Event` geprüft; `Show`/`Hide`/`Raise` vom Server zurückgelesen | +| Cocoa-Fenster | eine macOS-CI-Spur: ein Fenster geöffnet und gerendert, Pixel abgetastet, ein Klick und eine Taste synthetisiert; `Show`/`Hide`/`Raise` von einer anderen Goroutine aus an ein echtes `NSWindow` gerichtet und von AppKit zurückgelesen | +| Win32-Fenster | eine Windows-11-arm64-VM: ein echtes Fenster, das eine `VBox`, ein `Label` und einen `Button` rendert, drei injizierte Klicks treiben seinen Zähler über die Fensterprozedur von 0 auf 3 | +| Wayland-Fenster | gegen einen Fake-Compositor, der die Map- und Unmap-Regeln von xdg-shell durchsetzt | +| wasmbox-Fenster | Headless-Chromium steuert den echten wasmdesk-Compositor: die komponierten Pixel zurückgelesen, ein echter Klick an das Widget geleitet | +| Android | ein Android-15-arm64-Gerät: Blits nur geschädigter Bereiche gemessen, memfd-Dirty-Memory gemessen, der Barrierefreiheitsbaum auf dem Gerät gebenchmarkt | +| macOS-Tray | eine echte macOS-Sitzung: Das Symbol verlässt die Menüleiste, wenn das Programm endet, kehrt zurück, wenn es startet, und öffnet sein Menü | +| Linux-Tray | CI gegen einen echten Session-Bus: Der Name wird beansprucht, das Menü beantwortet `GetLayout`, `Quit` zieht es zurück | +| Windows-Tray | das Symbol und sein Menü auf einer Windows-11-VM | +| Terminal-Demos | ein echtes pty in der CI: Tasten-Bytes gesendet, der gerenderte Frame geprüft | + +## Kompiliert, nicht ausgeführt {#compiled-not-run} + +- Win32 `Show`/`Hide`/`Raise`: Die Entscheidungstabelle ist per Unit-Test geprüft, die Aufrufe + auf dem Thread des Fensters werden nur kompiliert. +- `Attach` des Windows-Trays. +- Die DOM-Schleife von `webcanvas` (`run_js.go`): hinter einem `js && wasm`-Build-Tag, also + außerhalb der gemessenen Abdeckung; die Anwendungen, die sie nutzen, üben sie aus. +- Das native `Run` von `application`: Das Öffnen eines echten Fensters ist von seiner + Coverage-Schranke ausgenommen; der Vertrag, die Ereignisübersetzung und der Bereitschaftszähler + sind abgedeckt. + +## Nicht umgesetzt {#not-done} + +- Die Trays unter Windows und Linux ignorieren `MenuItem.Icon`: Eine Zeile mit einem Symbol + wird ohne es gezeichnet. +- Das Wayland-Back-End von `window` kann kein `Raise`: xdg-shell hat keine solche Anfrage, + und xdg-activation braucht ein Token aus der eigenen Eingabe des Benutzers. +- Android: nur `android/arm64`, weil es das einzige Android-Ziel ist, das Go + ohne cgo linkt. +- `window.Screens` gibt unter `js/wasm` `ErrScreensUnsupported` zurück. + +## Was die CI noch nicht abdeckt {#what-ci-does-not-cover-yet} + +Gemessen an den Workflows jedes Repositorys in den oben genannten Tags: + +| Modul | Lücke | +|---|---| +| `mvvmtk` | keine Coverage-Schranke; Cross-Builds nur für amd64 und arm64, auf sechs Betriebssystemen | +| `application` | Cross-Builds für Linux, macOS und Windows nur auf amd64 | +| `tray` | Cross-Builds für vier Paare aus Betriebssystem und Architektur | +| `mvvmlint` | kein Cross-Build | +| `bricolint` | sein Schritt „6 arches" baut fünf: loong64 fehlt | + +`app-template` und `gallery` sind Browser-Anwendungen und bauen für `js/wasm`, +ihr einziges Ziel. diff --git a/content/de/surfaces/_index.md b/content/de/surfaces/_index.md new file mode 100644 index 0000000..5539ed6 --- /dev/null +++ b/content/de/surfaces/_index.md @@ -0,0 +1,31 @@ +--- +title: "Wohin es zeichnet" +linkTitle: "Oberflächen" +weight: 30 +description: "Jede Oberfläche, auf der ein go-widgets-Baum angezeigt werden kann, und was zwischen ihnen auswählt." +tags: [oberflächen, back-ends] +--- + +| Oberfläche | Modul | Painter | +|---|---|---| +| [Ein natives Fenster]({{< relref "/surfaces/native-window.md" >}}) unter X11, Wayland, macOS, Windows | `window` | Pixel | +| [Eine Anwendung]({{< relref "/surfaces/application.md" >}}) mit Tray-Symbol und dem Erscheinungsbild des Systems | `application`, `tray` | Pixel | +| [Ein Browser-Tab]({{< relref "/surfaces/browser.md" >}}): ein einfaches `` | `webcanvas` | Pixel | +| [Ein Browser-Desktop]({{< relref "/surfaces/browser.md#a-window-on-the-wasmdesk-desktop" >}}): ein Fenster des wasmdesk-Compositors | `window` (wasmbox) | Pixel | +| [Ein Terminal]({{< relref "/surfaces/terminal.md" >}}) | `tui` | Zellen | +| [Eine Android-APK]({{< relref "/surfaces/android.md" >}}) | `android`, `window` | Pixel | +| [Ein Bild]({{< relref "/surfaces/snapshots.md" >}}): SVG oder PNG | `svg` | Pixel | + +Der Widget-Baum ändert sich zwischen ihnen nicht. Was sich ändert, ist, wem der +Puffer gehört und wer die Eingaben liefert – ein Display-Server, ein Browser, ein Terminal, ein +Java-Host –, und jedes Back-End macht aus diesen Eingaben dasselbe `toolkit.Event`. + +## Ausgewählt nach dem, was vorhanden ist {#chosen-by-what-is-there} + +`window.Open` nimmt kein Back-End-Argument. Es betrachtet die Umgebung – die +Plattform, für die es gebaut wurde, `$WAYLAND_DISPLAY`, `$DISPLAY`, den Socket, den ein +Android-Host exportiert – und wählt dasjenige, das funktionieren kann. Ein für +Linux gebautes Binary öffnet unter einer Wayland-Sitzung ein Wayland-Fenster und unter +X ein X11-Fenster; dasselbe Android-Binary verbindet sich in einer APK mit seinem Host und öffnet unter Termux ein gewöhnliches +Linux-Fenster. Siehe +[wie `Open` auswählt]({{< relref "/surfaces/native-window.md#how-open-chooses" >}}). diff --git a/content/de/surfaces/android.md b/content/de/surfaces/android.md new file mode 100644 index 0000000..7ab9712 --- /dev/null +++ b/content/de/surfaces/android.md @@ -0,0 +1,63 @@ +--- +title: "Android" +linkTitle: "Android" +weight: 50 +description: "go-widgets/android: eine installierbare APK, deren gesamte Oberfläche von einem CGO_ENABLED=0-Go-Prozess angeordnet und gezeichnet wird." +tags: [oberflächen, android] +--- + +Android ist die einzige Plattform, auf der ein CGO-freier Prozess kein Fenster besitzen kann: Jeder +Weg zu einer zeichenbaren Oberfläche führt über JNI, und JNI braucht cgo. Es gibt kein +Wire-Protokoll, das man stattdessen sprechen könnte, so wie X11 und Wayland eines haben. + +Deshalb besteht [`go-widgets/android`](https://github.com/go-widgets/android) aus zwei +Prozessen: + +| | | +|---|---| +| **Java-Host** (`host/`) | besitzt die `Activity`, die `SurfaceView`, Touch, Tasten und den Lebenszyklus. Überträgt Pixel per Blit; weiß nichts über Widgets. | +| **Go-Anwendung** (`cmd/gwapp`) | ein gewöhnliches `CGO_ENABLED=0 GOOS=android`-Executable. Besitzt Layout, Widgets, Theme, Hit-Testing und Fokus, unverändert gegenüber jedem anderen Back-End. | + +Der Java-Host steht dort, wo unter Linux der X-Server steht. Pixel reisen durch +ein **memfd**, das die Anwendung anlegt und dem Host über den Socket übergibt; +die Go-Seite schreibt RGBA_8888, was Byte für Byte Androids ARGB_8888 entspricht, sodass +der Blit eine Kopie ohne Umwandlung ist, und zwar nur des geschädigten Rechtecks. Eingaben, +Insets, IME-Text und der Barrierefreiheitsbaum gehen über ein gerahmtes Protokoll über einen +abstrakten `LocalSocket`. + +```go +c, err := android.Dial("my app", nil) // nil theme: toolkit.DefaultDark() +if errors.Is(err, android.ErrUnsupported) { + return nil // not under a host +} +defer c.Close() +return c.Run(myWidgetTree()) +``` + +`Client` erfüllt das `Backend` von `window`, sodass eine Anwendung zwischen +Android und den Desktop-Back-Ends wechselt, ohne eine Zeile oberhalb des Fensters zu ändern – +und [`window.Open`]({{< relref "/surfaces/native-window.md#how-open-chooses" >}}) +wählt es von selbst, wenn der Socket des Hosts vorhanden ist. + +## Nur arm64 {#arm64-only} + +`android/arm64` ist das einzige Android-Ziel, das Go ohne cgo linkt; `arm`, +`amd64` und `386` erfordern externes Linken. Die CI prüft beide Hälften – das eine Ziel, +das baut, und die drei, die es nicht tun –, sodass der Build es meldet, sobald Go die Einschränkung aufhebt. + +## Barrierefreiheit {#accessibility} + +Ein Screenreader würde die `SurfaceView` als ein einziges undurchsichtiges Rechteck sehen. Der Host +gibt ihm stattdessen eine virtuelle View-Hierarchie: einen Knoten pro zugänglichem Widget, +mit der `android.widget.*`-Klasse, nach der Android seine Ansagen auswählt, dem +Text, den Grenzen und einer Aktivierungsaktion. Der Baum wird **abgeholt**, wenn +etwas ihn liest, nie gepusht, sodass eine App ohne angeschlossenen Barrierefreiheitsdienst +nie einen aufbaut. Eine Aktivierung kommt als gewöhnlicher Klick in der +Mitte des Elements zurück, über denselben Code, den eine Berührung nimmt. + +## Touch und Animation {#touch-and-animation} + +Jede Touch-Abtastung erreicht den Baum als Touch-Ereignis und dann als Maus-Ereignis: das +erste für gestenfähige Widgets, das zweite für die vielen, die nur auf +Klicks hören. Animierte Widgets laufen auf einer Frame-Schleife weiter, die startet, wenn etwas +zu animieren beginnt, und stoppt, wenn nichts mehr animiert. diff --git a/content/de/surfaces/application.md b/content/de/surfaces/application.md new file mode 100644 index 0000000..3e1706c --- /dev/null +++ b/content/de/surfaces/application.md @@ -0,0 +1,82 @@ +--- +title: "Eine Anwendung und ihr Tray" +linkTitle: "Anwendung und Tray" +weight: 20 +description: "go-widgets/application besitzt den Lebenszyklus oberhalb eines Fensters; go-widgets/tray setzt ein Symbol und ein Menü in die Menüleiste oder den Infobereich." +tags: [oberflächen, anwendung, tray] +--- + +## Die Anwendung {#the-application} + +[`go-widgets/application`](https://github.com/go-widgets/application) ist die +Schicht oberhalb von [`window`]({{< relref "/surfaces/native-window.md" >}}): Sie öffnet +das Fenster, treibt seine Schleife, hängt ein Tray-Symbol an, sobald der erste Frame steht, +reicht das aktuelle Erscheinungsbild des Systems (dunkel oder hell, Akzentfarbe, Systemschrift) +an die Anwendung weiter, veröffentlicht den Barrierefreiheitsbaum und ruft `onReady` auf, +sobald der erste Frame auf dem Bildschirm ist. + +```go +spec := application.Spec{ + Name: "News Reader", + Identifier: "com.example.reader", + Version: "1.0.0", + Tray: func() *tray.Menu { + return tray.NewMenu().Add( + tray.Item("Refresh", refresh), + tray.Item("Quit", quit), + ) + }, +} +cfg := application.Config{Title: "News Reader", Width: 1200, Height: 800} +err := application.Run(spec, cfg, handler, func() { log.Println("on screen") }) +``` + +`handler` implementiert `Handler` – `Frame`, `Resize`, Maus-, Scroll- und Tastatur- +Ereignisse – und kann jede der optionalen Fähigkeiten implementieren: Erscheinungsbild, +Tastenkürzel, Klicks und Tasten mit Modifikatoren, Sekundärklick, Kontextmenüs, +Zwischenablage, Barrierefreiheit, native Bedienelemente. Die Schleife umhüllt den Handler nie; +sie macht für jede Fähigkeit eine Typzusicherung und beachtet die, die sie findet. Ein Test durchläuft +das Paket nach diesen Zusicherungen und schlägt fehl, wenn die Paketdokumentation +nicht jede einzelne nennt. + +Ein Tray, das nicht angehängt werden kann, ist ein Fehler von `Run`, kein Schweigen: +`errors.Is(err, tray.ErrNoBackend)` sagt, warum. Eine Anwendung, die ihr +Fenster will, egal was mit dem Tray geschieht, lässt `Spec.Tray` auf nil und betreibt selbst +eines. + +`Run` schließt sein Fenster, wenn die Schleife zurückkehrt, sodass das „Open" einer Tray-Anwendung +`Run` erneut aufrufen kann, um ein frisches Fenster zu bekommen. Für einen Host, der kein +Fenster ist – eine Desktop-Shell, ein Browser-Tab –, geben `Bind` und `BindScaled` eine +`*toolkit.Surface` zurück, die mit demselben Handler verdrahtet ist. + +## Das Tray {#the-tray} + +Ein Tray-Symbol ist Betriebssystemintegration, kein gezeichnetes Widget, deshalb +steuert [`go-widgets/tray`](https://github.com/go-widgets/tray) die eigene API jeder Plattform, +weiterhin mit `CGO_ENABLED=0`: + +| Plattform | API | Über | +|---|---|---| +| macOS | `NSStatusItem`, `NSMenu` | purego und die Objective-C-Runtime | +| Windows | `Shell_NotifyIcon`, `TrackPopupMenu` | `golang.org/x/sys/windows` | +| Linux | `StatusNotifierItem`, `com.canonical.dbusmenu` | DBus, in Go | + +```go +menu := tray.NewMenu().Add( + tray.Item("Open", open), + tray.Checkbox("Notifications", true, setNotify), + tray.SubMenu("Recent", tray.NewMenu().Add(tray.Item("file.txt", nil))), + tray.Separator(), + tray.Item("Quit", func() { t.Quit() }), +) +t := tray.New(iconPNG).SetTooltip("My App").SetMenu(menu) +t.Run() // blocks on the platform's loop until Quit +``` + +Ein Programm, das bereits die Schleife eines Fensters betreibt, kann `Run` seinen Hauptthread nicht überlassen: +`Attach` zeigt dasselbe Symbol an und kehrt zurück, und `Quit` entfernt es wieder. + +Die nativen Back-Ends sind standardmäßig aktiv; es gibt kein Build-Tag, an das man denken müsste. +Auf einer Plattform ohne Back-End gibt `Run` `ErrNoBackend` zurück – der Unterschied +zwischen „hier gibt es kein Tray" und einem Tray, das stillschweigend nichts tut. Ein Test +oder ein Headless-Dienst übergibt `WithBackend(tray.NewHeadless())`. diff --git a/content/de/surfaces/browser.md b/content/de/surfaces/browser.md new file mode 100644 index 0000000..d1c584a --- /dev/null +++ b/content/de/surfaces/browser.md @@ -0,0 +1,54 @@ +--- +title: "Ein Browser-Tab" +linkTitle: "Browser" +weight: 30 +description: "Einen Widget-Baum in einem einfachen mit go-widgets/webcanvas ausführen oder als Fenster des wasmdesk-Compositors." +tags: [oberflächen, wasm, browser, webcanvas, wasmdesk] +--- + +Das Toolkit ist reines Go ohne DOM-Abhängigkeit, daher baut `GOOS=js GOARCH=wasm` +es wie jedes andere Ziel. Zwei Wege, das Ergebnis jemandem vorzuführen: + +## Ein einfaches Canvas {#a-plain-canvas} + +[`go-widgets/webcanvas`](https://github.com/go-widgets/webcanvas) überträgt einen RGBA- +Framebuffer per Blit in ein `` und leitet DOM-Zeiger- und Tastaturereignisse zurück +in die Szene. Kein Compositor, kein `SharedArrayBuffer`, keine Header für Cross-Origin- +Isolation: Jeder statische Host kann es ausliefern. + +```go +//go:build js && wasm + +package main + +import "github.com/go-widgets/webcanvas" + +func main() { webcanvas.Run("screen", myScene()) } +``` + +Die Szene implementiert `App`: `Size`, `Draw(buf []byte)` und eine Methode pro +Ereignisart, die jeweils meldet, ob sich etwas geändert hat, damit ein unveränderter Frame +nicht neu gezeichnet wird. Sie kann außerdem `Ticker`, `Animator`, `Resizer` oder +`Scroller` implementieren. + +Die [Galerie](https://go-widgets.github.io/gallery/) und +[`app-template`]({{< relref "/getting-started.md" >}}) laufen auf diese Weise. + +## Ein Fenster auf dem wasmdesk-Desktop {#a-window-on-the-wasmdesk-desktop} + +Unter `js/wasm` gibt [`window.Open`]({{< relref "/surfaces/native-window.md" >}}) +einen Client des Browser-Compositors [wasmdesk/wasmbox](https://github.com/wasmdesk/wasmbox) +zurück. Er legt seine Oberfläche in einem `SharedArrayBuffer` an, sagt +`hello` über seinen `MessagePort`, wartet auf `welcome`, zeichnet in den gemeinsamen +Puffer und sendet `commit` – die ganze Oberfläche oder nur die geschädigten Rechtecke. +Die `input`-Nachrichten des Compositors werden zu `toolkit.Event`, wie unter X11. + +So läuft eine gegen `window` geschriebene Anwendung unverändert als natives Fenster +und als Fenster eines Desktops in einem Browser-Tab. Die +[Desktop-Shell-Demo](https://github.com/go-widgets/desktop) macht genau +das, aus einer einzigen Quelle. + +Der Protokoll-Codec ist auf jeder Plattform zu 100 % abgedeckt. Der Live-Nachweis lässt +Headless-Chromium gegen den echten wasmdesk-Compositor laufen: Er startet den Client, +liest die vom Compositor selbst komponierten Pixel, klickt und prüft, dass der +Klick das Widget erreicht hat. diff --git a/content/de/surfaces/native-window.md b/content/de/surfaces/native-window.md new file mode 100644 index 0000000..715a0d1 --- /dev/null +++ b/content/de/surfaces/native-window.md @@ -0,0 +1,90 @@ +--- +title: "Ein natives Fenster" +linkTitle: "Natives Fenster" +weight: 10 +description: "go-widgets/window: eine einzige Open/Run-API über X11, Wayland, Cocoa, Win32, GTK4, Android und wasmbox, alle mit CGO_ENABLED=0." +tags: [oberflächen, fenster, x11, wayland, macos, windows] +--- + +[`go-widgets/window`](https://github.com/go-widgets/window) öffnet ein echtes Fenster +und führt darin einen Widget-Baum aus: Layout, Zeichnen, Darstellen, Eingaben in +`toolkit.Event` übersetzen, wiederholen, bis das Fenster geschlossen wird. + +```go +w, err := window.Open(window.Config{Title: "Demo"}) +if err != nil { + return err +} +defer w.Close() +return w.Run(root) // root is any toolkit.Widget +``` + +`Config` nimmt außerdem die Anfangsgröße in **logischen Punkten** (null bittet um einen +lesbaren Standardwert), Instanz und Klasse für `WM_CLASS`, ein `Theme` und eine +`RenderScale`. + +## Wie Open auswählt {#how-open-chooses} + +| Plattform und Umgebung | Back-End | Wie es auf den Bildschirm kommt | +|---|---|---| +| Linux, `$GO_WIDGETS_GTK` gesetzt | GTK4 | GTK besitzt das Fenster; der Framebuffer ist ein `GtkPicture`, und native Bedienelemente sind echte GTK-Widgets darüber. Benötigt die libgtk-4-Laufzeitbibliothek. | +| Linux, `$WAYLAND_DISPLAY` gesetzt | Wayland | xdg-shell über den Unix-Socket des Compositors | +| Linux, sonst `$DISPLAY` | X11 | das X11-Kernprotokoll über den Unix-Socket, mit MIT-SHM | +| macOS | Cocoa | `NSWindow` und `NSView` über [go-macos/objc](https://github.com/go-macos/objc) (purego) | +| Windows | Win32 | ein Top-Level-`HWND` über Syscalls von user32 und gdi32, `StretchDIBits` | +| Android, `$GW_ANDROID_SOCKET` gesetzt | Android-Host | ein gerahmtes Protokoll zum [Java-Host]({{< relref "/surfaces/android.md" >}}), Pixel in einem gemeinsamen memfd | +| Android, sonst | Wayland oder X11 | eine Shell unter Termux hat trotzdem einen Display-Server, mit dem sie sich verbinden kann | +| `js/wasm` | wasmbox | ein Client des [wasmdesk-Compositors]({{< relref "/surfaces/browser.md#a-window-on-the-wasmdesk-desktop" >}}) | +| alles andere | — | `window.ErrUnsupported`, damit ein Cross-Build trotzdem kompiliert | + +Jedes Back-End ist `CGO_ENABLED=0`. Das X11-Back-End ist das Kernprotokoll, von Grund auf +über dem Socket geschrieben – kein Xlib, kein XCB –, mit beiden Byte-Reihenfolgen, dem +Xauthority-Cookie, Keysym-Zuordnung, `PutImage` in Kacheln unterhalb der +Anfragegröße des Servers und dem schnellen MIT-SHM-Pfad, wobei das gemeinsame Segment über +`SCM_RIGHTS` übergeben wird. Windows wird über die prozesseigenen DLLs mit +`syscall.NewLazyDLL` und einer Fensterprozedur per `syscall.NewCallback` erreicht; macOS +über die Objective-C-Runtime mit purego. + +Der plattformunabhängige Teil jedes Back-Ends – Ereigniszuordnung, Koordinaten- +mathematik, Pixel-Packing, Schadensrechtecke – liegt in einem Codec, der zu 100 % abgedeckt ist, +auf jedem Betriebssystem. Die dünne Plattformanbindung ist live nachgewiesen: siehe +[Stand]({{< relref "/status.md" >}}). + +## Nur was sich geändert hat {#only-what-changed} + +Eine Wurzel, die `DamageRenderer` implementiert (wie `toolkit/scene.HostRoot`), +meldet die Rechtecke, die sie neu gezeichnet hat, und die Back-Ends für Cocoa, Win32 und wasmbox +stellen nur diese dar. + +## HiDPI {#hidpi} + +Standard ist ein Framebuffer-Pixel pro logischem Punkt: Die UI wird in einer +lesbaren Größe angeordnet und gezeichnet, und der Compositor skaliert sie hoch. Unter Windows +deklariert das Fenster DPI-Bewusstsein pro Monitor, und das Betriebssystem skaliert den logischen +Frame auf den physischen Client-Bereich. `RenderScale` fordert einen Framebuffer in +der echten Auflösung des Bildschirms an, was nur für eine Wurzel korrekt ist, die in +Gerätepixeln anordnet. + +## Bildschirme, Anzeigen und Ausblenden {#screens-and-showing-and-hiding} + +`window.Screens()` listet die angeschlossenen Displays auf, das primäre zuerst, in logischen +Punkten und ohne die Leisten des Desktops, und kann vor `Open` aufgerufen werden. +Der Name eines Bildschirms ist der des Panels selbst (`"DELL U2720Q"`, aus seiner EDID), mit +Rückfall auf den Anschluss (`"HDMI-1"`), wenn es keinen veröffentlicht – und auch, wenn zwei +angeschlossene Panels denselben veröffentlichen, denn ein Name, der sie nicht +unterscheiden kann, ist kein Name. + +`window.Show`, `Hide` und `Raise` nehmen ein offenes Fenster vom Bildschirm, ohne es +zu schließen, und holen es zurück – was eine [Tray-Anwendung]({{< relref "/surfaces/application.md#the-tray" >}}) +braucht. + +| Back-End | Show | Hide | Raise | +|---|---|---|---| +| X11 | `MapWindow` | ICCCM-Withdraw, sodass es die Taskleiste verlässt | Map, Raise, `_NET_ACTIVE_WINDOW` | +| Wayland | erneuter Commit | Null-Puffer und Commit | `ErrNotSupported`: xdg-shell hat keine solche Anfrage | +| macOS | `orderFront:` | `orderOut:` | die App aktivieren, `makeKeyAndOrderFront:` | +| Windows | `SW_SHOWNA` | `SW_HIDE` | `SW_RESTORE` und `SetForegroundWindow` | +| GTK, Android, wasmbox | `ErrNotSupported` | `ErrNotSupported` | `ErrNotSupported` | + +Den Fokus gewährt die Plattform: Unter einem Schutz gegen Fokusraub kann `Raise` +das Fenster möglicherweise nur als aufmerksamkeitsbedürftig markieren und kann das nicht erkennen. diff --git a/content/de/surfaces/snapshots.md b/content/de/surfaces/snapshots.md new file mode 100644 index 0000000..4eff708 --- /dev/null +++ b/content/de/surfaces/snapshots.md @@ -0,0 +1,31 @@ +--- +title: "Snapshots: ein Widget als SVG oder PNG" +linkTitle: "Snapshots" +weight: 60 +description: "go-widgets/svg verpackt jedes RGBA-Rendering bitgenau in ein eigenständiges SVG oder schreibt es als PNG." +tags: [oberflächen, svg, png] +--- + +[`go-widgets/svg`](https://github.com/go-widgets/svg) macht aus einem RGBA-Puffer +ein eigenständiges SVG: eine `viewBox`, die auf die Pixelgröße des Renderings festgelegt ist, und die +Pixel als Base64-PNG unter `image-rendering: pixelated`. Jedes angezeigte Pixel +ist das Pixel, das das Widget gezeichnet hat – keine Re-Vektorisierung, kein Schriftersatz –, und +es bleibt bei jedem Zoom scharf. + +```go +// any RGBA producer +svg.Snapshot(f, surface, w, h, "my image") + +// a toolkit widget +btn := toolkit.NewButton("Click me", nil) +btn.SetBounds(toolkit.Rect{W: 200, H: 40}) +widget.Snapshot(f, btn, 200, 40, toolkit.DefaultLight(), "widget: button") +widget.PNG(p, btn, 200, 40, toolkit.DefaultLight()) +``` + +Die Beschriftung wird, maskiert, zu `` und `aria-label` des SVG. Das Wurzelpaket +hängt von nichts ab; das Unterpaket `widget` ist getrennt, damit ein Nutzer +mit eigenem Pixel-Erzeuger das Toolkit nicht in seinen Modulgraphen zieht. + +`svg/cmd/gallery-render` rendert den Widget-Katalog, hell und dunkel: +[go-widgets.github.io/svg](https://go-widgets.github.io/svg/). diff --git a/content/de/surfaces/terminal.md b/content/de/surfaces/terminal.md new file mode 100644 index 0000000..b792fde --- /dev/null +++ b/content/de/surfaces/terminal.md @@ -0,0 +1,64 @@ +--- +title: "Ein Terminal" +linkTitle: "Terminal" +weight: 40 +description: "go-widgets/tui rendert Widgets über den Zellen-Painter: ein Snapshot-Renderer, ein interaktiver Runner und Widgets, die für ein Zeichenraster bemessen sind." +tags: [oberflächen, tui, terminal] +--- + +[`go-widgets/tui`](https://github.com/go-widgets/tui) macht aus einem Widget-Baum +einen Terminal-Frame, über den [Zellen-Painter]({{< relref "/rendering/painter.md" >}}). +Nur Standardbibliothek – kein `x/term`, kein cgo. + +## Ein Frame {#one-frame} + +```go +_ = tui.RenderOnce(os.Stdout, widgets, nil) // nil theme: light +``` + +`RenderOnceSized(w, cols, rows, widgets, theme)` nimmt die Größe explizit entgegen; +`RenderOnce` liest `COLUMNS` und `LINES` und fällt auf 80×24 zurück. Die +Umgebung zu lesen, statt `TIOCGWINSZ` aufzurufen, hält das Paket frei von +plattformspezifischem Code. + +## Ein interaktives Programm {#an-interactive-program} + +```go +app := tui.NewApp() +app.Root = buildScene() +app.Keys["q"] = func(a *tui.App) { a.Quit() } +app.Keys["Ctrl+C"] = func(a *tui.App) { a.Quit() } +os.Exit(app.Run()) +``` + +`Run` wechselt in den alternativen Bildschirm und den Raw-Modus, leitet Eingaben an `Root` weiter +(globale Tasten-Handler zuerst), folgt `SIGWINCH` und stellt das Terminal immer wieder her – +auch bei einer Panic. Ein Tasten-Handler ruft `Consume` auf, um ein Ereignis von +`Root` fernzuhalten; `InputTarget` schickt jedes nicht konsumierte Ereignis an ein einzelnes Widget, und so +verschluckt eine Befehlspalette Tastatureingaben, solange sie offen ist. `NewFocusRing` gibt einem +Formular Navigation mit Tab und Shift+Tab. + +## Widgets, für Zellen bemessen {#widgets-sized-for-cells} + +Die meisten Toolkit-Widgets sind in Pixeln entworfen: Ihre Padding-Konstanten zählen unter dem +Zellen-Painter als Zellen und blähen sich auf. Deshalb hat `tui` einen eigenen **zellennativen** +Satz – `TextEditor`, `Entry`, `Button`, `CheckButton`, `RadioButton`, `Scale`, +`ProgressBar`, `Sparkline`, `ListBox`, `TreeView`, `Table`, `MenuBar`, +`Notebook`, `HSplit`, `VSplit`, `Dialog`, `Dropdown`, `Spinner` und weitere –, +in dem eine Glyphe eine Zelle ist. Jedes davon ist weiterhin ein `toolkit.Widget`, sodass dieselbe +Instanz auch in Pixel zeichnet. + +`TextEditor` ist ein Code-Editor zum Lesen und Schreiben: Randspalte, Rückgängig und Wiederherstellen, Suchen und +Ersetzen, Auswahl, Zeilen verschieben und Syntaxhervorhebung für Go (über +`go/scanner`), JavaScript und TypeScript, Python, Ruby, Shell, C und C++, +Rust, JSON, YAML, HCL, TOML, LaTeX und Markdown, ohne externe Abhängigkeit. + +```sh +go run ./cmd/tui-widgets | less -R # every cell-native widget, one per slot +go run ./cmd/tui-widget-explorer # the same, interactive +go run ./cmd/tui-explorer # a file browser +go run ./cmd/tui-editor --file=x.go # a modal editor +``` + +Die beiden Demoprogramme werden Ende-zu-Ende in einem echten pty getestet: echte Tasten-Bytes hinein, +der gerenderte Frame geprüft. diff --git a/content/en/_index.md b/content/en/_index.md new file mode 100644 index 0000000..e1d5dc2 --- /dev/null +++ b/content/en/_index.md @@ -0,0 +1,76 @@ +--- +title: "go-widgets" +linkTitle: "Home" +type: docs +cascade: + type: docs +description: "A pure-Go widget toolkit and every surface it paints on: a native window on X11, Wayland, macOS and Windows, a browser canvas, a terminal, an Android APK." +--- + +**One widget tree, painted by Go, on every surface Go reaches.** A native +window on X11, Wayland, macOS and Windows; a `<canvas>` in a browser tab; a +terminal; an Android APK. No C toolchain, no webview, no system widget set: +every module builds with `CGO_ENABLED=0`, and every pixel is drawn by the +toolkit. + +```go +w, err := window.Open(window.Config{Title: "Hello"}) +if err != nil { + return err +} +defer w.Close() + +box := toolkit.NewVBox() +box.Append(toolkit.NewLabel("Hello from go-widgets")) +box.Append(toolkit.NewButton("Click me", func() { /* ... */ })) +return w.Run(box) // an X11 or Wayland window, an NSWindow, a Win32 window +``` + +That is +[`cmd/windowdemo`](https://github.com/go-widgets/window/blob/main/cmd/windowdemo/main.go), +shortened. The same `box` drawn in a browser, a terminal or a PNG is the same +code with a different surface underneath it. + +## Why one toolkit draws everything + +A widget that calls the platform's own button is as portable as the least +capable platform it targets, and looks different on each. go-widgets goes the +other way: a widget's `Draw` only ever talks to a +[`painter.Painter`]({{< relref "/rendering/painter.md" >}}), a handful of +primitives (fill a rectangle, stroke one, set a pixel, draw text). Whether +those calls become RGBA pixels or terminal cells is the back-end's business, +not the widget's. + +What that costs is said plainly: no native look by default, and no free +accessibility — the toolkit publishes its own +[accessibility tree]({{< relref "/rendering/toolkit.md#accessibility" >}}) +to each platform. What it buys is one widget set, tested once, with the same +behaviour everywhere, and a binary that cross-compiles from any machine to +any of them. + +## Where to go next + +| | | +|---|---| +| [A first application]({{< relref "/getting-started.md" >}}) | from `app-template`, or from a bare window | +| [How a widget reaches the screen]({{< relref "/rendering/_index.md" >}}) | the painter seam, the widget set, skins | +| [The painter seam]({{< relref "/rendering/painter.md" >}}) | the primitives every widget is drawn with | +| [The toolkit]({{< relref "/rendering/toolkit.md" >}}) | 160 widget types, themes, layouts, text, accessibility | +| [Skins]({{< relref "/rendering/skin.md" >}}) | a widget's parts and states in a data file | +| [State lives in a view-model]({{< relref "/state/_index.md" >}}) | why widget fields are not assigned by hand | +| [MVVM]({{< relref "/state/mvvm.md" >}}) | observables, commands, binders, undo | +| [The data spine]({{< relref "/state/data.md" >}}) | typed records, queries, a local or remote store | +| [Where it paints]({{< relref "/surfaces/_index.md" >}}) | every back-end and how it is chosen | +| [A native window]({{< relref "/surfaces/native-window.md" >}}) | X11, Wayland, Cocoa, Win32, GTK4, Android, wasmbox | +| [An application and its tray]({{< relref "/surfaces/application.md" >}}) | lifecycle, appearance, a menu-bar icon | +| [A browser tab]({{< relref "/surfaces/browser.md" >}}) | a plain `<canvas>`, or a wasmdesk window | +| [A terminal]({{< relref "/surfaces/terminal.md" >}}) | cell-native widgets and an interactive runner | +| [Android]({{< relref "/surfaces/android.md" >}}) | a real APK painted by a CGO-free process | +| [Snapshots]({{< relref "/surfaces/snapshots.md" >}}) | a widget as an SVG or a PNG | +| [Rules CI enforces]({{< relref "/guard-rails/_index.md" >}}) | `mvvmlint` and `bricolint` | +| [Modules]({{< relref "/modules.md" >}}) | all nineteen, with their sources and references | +| [Status]({{< relref "/status.md" >}}) | what is verified where, and what is not done | + +## Licence + +BSD-3-Clause. diff --git a/content/en/getting-started.md b/content/en/getting-started.md new file mode 100644 index 0000000..17e0d2d --- /dev/null +++ b/content/en/getting-started.md @@ -0,0 +1,79 @@ +--- +title: "A first application" +weight: 5 +description: "Start from app-template, already MVVM-compliant and green, or open a bare window and add widgets." +tags: [getting-started, app-template, window] +--- + +## From the template + +[`go-widgets/app-template`](https://github.com/go-widgets/app-template) is a +template repository: a small but real browser application with a search +field, a category filter, a list of the rows that match and a status line. +Everything a larger application needs is already wired: a view-model with no +widget in it, a view bound to it, a wasm host, a 100% coverage gate, and the +[MVVM gate]({{< relref "/guard-rails/mvvmlint.md" >}}). + +```sh +git clone https://github.com/go-widgets/app-template my-app +cd my-app +go mod edit -module github.com/you/my-app +./build.sh # dist/app.wasm, dist/wasm_exec.js, dist/index.html +``` + +Serve `dist/` over HTTP: a page opened from `file://` cannot instantiate wasm. + +| File | What it holds | +|---|---| +| `viewmodel.go` | every piece of state, as [`mvvm`]({{< relref "/state/mvvm.md" >}}) observables and commands. No widget. Testable with no canvas. | +| `scene.go` | the widgets, each bound to the view-model through `mvvmtk`. It never assigns a widget's state field itself. | +| `main.go` | the only file with a build tag (`js && wasm`): forwards input to the toolkit and blits the frame into a `<canvas>`. | + +Edit the first two and keep state flowing through the binders: the +`mvvm` job in CI fails a pull request that assigns a widget's field directly. + +## From a bare window + +```go +package main + +import ( + "github.com/go-widgets/toolkit" + "github.com/go-widgets/window" +) + +func main() { + w, err := window.Open(window.Config{Title: "Demo"}) + if err == window.ErrUnsupported { + return // no native back-end on this platform + } + if err != nil { + panic(err) + } + defer w.Close() + + box := toolkit.NewVBox() + box.Append(toolkit.NewLabel("Hello")) + box.Append(toolkit.NewButton("Click me", func() {})) + w.Run(box) // layout, draw, present, dispatch input, until closed +} +``` + +Width and height left at zero ask the back-end for a readable default; on +macOS that is a fraction of the main screen. `CGO_ENABLED=0 go build` produces +the binary for each platform, from any machine. + +## Which module for what + +| You want | Start with | +|---|---| +| a desktop window | [`window`]({{< relref "/surfaces/native-window.md" >}}) | +| a desktop application with a tray icon and the system appearance | [`application`]({{< relref "/surfaces/application.md" >}}) | +| a page in a browser | [`webcanvas`]({{< relref "/surfaces/browser.md" >}}), or the template above | +| a terminal program | [`tui`]({{< relref "/surfaces/terminal.md" >}}) | +| an Android app | [`android`]({{< relref "/surfaces/android.md" >}}) | +| a picture of a widget for a README | [`svg`]({{< relref "/surfaces/snapshots.md" >}}) | + +To see the widget set before writing anything, open the +[gallery](https://go-widgets.github.io/gallery/): every widget family, live, +in a browser canvas. diff --git a/content/en/guard-rails/_index.md b/content/en/guard-rails/_index.md new file mode 100644 index 0000000..848588e --- /dev/null +++ b/content/en/guard-rails/_index.md @@ -0,0 +1,24 @@ +--- +title: "Rules CI enforces" +linkTitle: "CI rules" +weight: 40 +description: "Two go/analysis checks an application runs as required status checks: state through MVVM, and no hand-drawn UI." +tags: [ci, lint] +--- + +Two conventions keep an application's UI honest, and both would erode one +convenient shortcut at a time if they were only written down. So each is a +[`go/analysis`](https://pkg.go.dev/golang.org/x/tools/go/analysis) analyzer, +run through `go vet`, with a reusable workflow an application marks as a +**required** check. + +| | Refuses | +|---|---| +| [`mvvmlint`]({{< relref "/guard-rails/mvvmlint.md" >}}) | assigning a widget's state field by hand, and a package with widgets but no view-model | +| [`bricolint`]({{< relref "/guard-rails/bricolint.md" >}}) | painter primitives in application code, and a widget built anew on every frame | + +Both are conservative by construction: a rule fires only when the **static +type** of the receiver resolves, through `go/types`, to a toolkit or painter +type. A same-named field or method on an unrelated type is never flagged. The +intended exceptions are explicit directives in the code, so each one is +written down where it is made. diff --git a/content/en/guard-rails/bricolint.md b/content/en/guard-rails/bricolint.md new file mode 100644 index 0000000..88680c1 --- /dev/null +++ b/content/en/guard-rails/bricolint.md @@ -0,0 +1,53 @@ +--- +title: "bricolint: no hand-drawn UI" +linkTitle: "bricolint" +weight: 20 +description: "Fails a pull request that paints chrome with painter primitives in application code, or rebuilds a widget on every frame." +tags: [ci, lint, painter] +--- + +*Bricolage* — improvised, hand-rolled drawing — is how an interface quietly +loses its pressed, hover and focus feedback, its theming, its HiDPI scaling +and its accessibility. [`go-widgets/bricolint`](https://github.com/go-widgets/bricolint) +keeps an application that has moved onto the toolkit from drifting back. + +```go +p.FillRect(bar, theme.Bg) // flagged: hand-drawn chrome +toolkit.NewBackdrop(theme.Bg).Draw(p) // a toolkit widget + +func (v *view) Draw(p painter.Painter) { + b := toolkit.NewButton("ok") // flagged: a new widget every frame + b.Draw(p) +} +``` + +1. **A painter primitive in application code**: a drawing method (`FillRect`, + `StrokeRect`, `FillPath`, `DrawImage`, `Text`, `PutPixel`, …) called on a + receiver whose static type is a painter type. Querying or clipping the + surface (`Size`, `PushClip`, `PushTranslate`) is not drawing and is not + flagged. `-primitives=` replaces the list. +2. **A widget thrown away every frame**: a `toolkit.New…` call inside a method + named `Draw`, `Paint` or `Render`. A widget rebuilt on each paint keeps no + interaction state. Build it once, keep it in a field, drive it through a + binding. `-checkthrow=false` turns this off. + +Some code is a genuine leaf — a game framebuffer, a painter back-end, the +toolkit's own internals — and says so explicitly: + +```go +p.FillRect(bg, c) //bricolint:allow engine SVG raster blit — a genuine leaf +``` + +```go +//bricolint:allowfile painter back-end — this file IS the leaf +package pdfsurface +``` + +**The reason is mandatory**: a directive without one is ignored, and the +finding keeps firing until somebody writes the justification down. + +```yaml +jobs: + bricolint: + uses: go-widgets/bricolint/.github/workflows/bricolint.yml@main +``` diff --git a/content/en/guard-rails/mvvmlint.md b/content/en/guard-rails/mvvmlint.md new file mode 100644 index 0000000..e7677e5 --- /dev/null +++ b/content/en/guard-rails/mvvmlint.md @@ -0,0 +1,44 @@ +--- +title: "mvvmlint: state goes through the view-model" +linkTitle: "mvvmlint" +weight: 10 +description: "Fails a pull request that assigns a toolkit widget's state field directly, or that has widgets and no view-model." +tags: [ci, lint, mvvm] +--- + +[`go-widgets/mvvmlint`](https://github.com/go-widgets/mvvmlint) fires only in +packages that import the toolkit. + +1. **A widget's state field assigned by hand.** `entry.Text = "x"`, where the + static type of `entry` is a toolkit widget and `Text` is a state field, is + flagged. `&entry.Text` — what the [binders]({{< relref "/state/mvvm.md" >}}) + take — is not an assignment and is never flagged; neither is a composite + literal (`toolkit.Entry{Text: "x"}`), nor composing a callback such as + `OnChange`. +2. **Widgets and no view-model.** A package that imports the toolkit but not + `go-widgets/mvvm` gets one diagnostic. `-requirevm=false` turns this off for + a widget library that legitimately has no view-model. + +The state fields are those of the toolkit's widgets (`Text`, `Items`, `Rows`, +`Root`, `Selected`, `Value`, `Checked`, …); `-statefields=` replaces the list. +A file named `*_binding.go`, or containing `//mvvmlint:allow`, is exempt from +the first rule. + +```sh +go install github.com/go-widgets/mvvmlint/cmd/mvvmlint@latest +go vet -vettool="$(go env GOPATH)/bin/mvvmlint" ./... +``` + +```yaml +# .github/workflows/mvvm.yml +name: mvvm +on: + pull_request: + push: + branches: [main] +jobs: + mvvm: + uses: go-widgets/mvvmlint/.github/workflows/mvvmlint.yml@main +``` + +[`app-template`]({{< relref "/getting-started.md" >}}) ships with this job. diff --git a/content/en/modules.md b/content/en/modules.md new file mode 100644 index 0000000..d3a87ed --- /dev/null +++ b/content/en/modules.md @@ -0,0 +1,47 @@ +--- +title: "Modules" +weight: 50 +description: "The nineteen modules of go-widgets, what each one is for, and where its source and API reference live." +tags: [modules] +--- + +The API reference is generated from the source by +[pkg.go.dev](https://pkg.go.dev/), which follows the released tags; these +pages explain, and do not copy it. The tag is the newest one at the time of +writing — the repository's own tag list is the authority. + +| Module | Tag | What it is | Reference | +|---|---|---|---| +| [`painter`](https://github.com/go-widgets/painter) | v0.15.0 | [the drawing seam]({{< relref "/rendering/painter.md" >}}): pixel and cell painters | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/painter) | +| [`toolkit`](https://github.com/go-widgets/toolkit) | v0.328.0 | [the widget set]({{< relref "/rendering/toolkit.md" >}}), themes, layouts, text | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/toolkit) | +| [`skin`](https://github.com/go-widgets/skin) | v0.2.0 | [widgets described as data]({{< relref "/rendering/skin.md" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/skin) | +| [`isoicons`](https://github.com/go-widgets/isoicons) | v0.3.0 | isometric icon packs (cloud-native, AWS) for the toolkit's isometric diagrams | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/isoicons) | +| [`mvvm`](https://github.com/go-widgets/mvvm) | v0.13.0 | [observables, commands, binders, undo]({{< relref "/state/mvvm.md" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/mvvm) | +| [`mvvmtk`](https://github.com/go-widgets/mvvmtk) | v0.14.1 | [one-call binders for toolkit widgets]({{< relref "/state/mvvm.md#mvvmtk-one-call-per-widget" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/mvvmtk) | +| [`data`](https://github.com/go-widgets/data) | v0.3.0 | [typed records, queries, a local or remote store]({{< relref "/state/data.md" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/data) | +| [`window`](https://github.com/go-widgets/window) | v0.86.1 | [a native window]({{< relref "/surfaces/native-window.md" >}}) on seven back-ends | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/window) | +| [`application`](https://github.com/go-widgets/application) | v0.7.0 | [the application lifecycle]({{< relref "/surfaces/application.md" >}}) above a window | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/application) | +| [`tray`](https://github.com/go-widgets/tray) | v0.14.0 | [a tray icon and its menu]({{< relref "/surfaces/application.md#the-tray" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/tray) | +| [`webcanvas`](https://github.com/go-widgets/webcanvas) | v0.2.0 | [a scene in a browser `<canvas>`]({{< relref "/surfaces/browser.md" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/webcanvas) | +| [`tui`](https://github.com/go-widgets/tui) | v0.61.0 | [terminal rendering and cell-native widgets]({{< relref "/surfaces/terminal.md" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/tui) | +| [`android`](https://github.com/go-widgets/android) | v0.15.0 | [an APK painted by a CGO-free process]({{< relref "/surfaces/android.md" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/android) | +| [`svg`](https://github.com/go-widgets/svg) | v0.6.0 | [a render as SVG or PNG]({{< relref "/surfaces/snapshots.md" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/svg) | +| [`mvvmlint`](https://github.com/go-widgets/mvvmlint) | v0.4.0 | [the MVVM check]({{< relref "/guard-rails/mvvmlint.md" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/mvvmlint) | +| [`bricolint`](https://github.com/go-widgets/bricolint) | v0.4.0 | [the no-hand-drawn-UI check]({{< relref "/guard-rails/bricolint.md" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/bricolint) | +| [`app-template`](https://github.com/go-widgets/app-template) | v0.4.0 | [the starting point for an application]({{< relref "/getting-started.md" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/app-template) | +| [`gallery`](https://github.com/go-widgets/gallery) | v0.6.0 | the [live demo](https://go-widgets.github.io/gallery/) of every widget family | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/gallery) | +| [`desktop`](https://github.com/go-widgets/desktop) | v0.19.0 | a desktop shell composing go-freedesktop and go-widgets, native and in wasmdesk | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/desktop) | + +## What every module is held to + +- `CGO_ENABLED=0`: no cgo, and no shelling out to a command-line tool in place + of a library. +- Cross-compiled for amd64, arm64, riscv64, loong64, ppc64le and s390x — the + last big-endian, which keeps every wire encoding honest — and, where it + applies, `js/wasm`. Twelve of the nineteen do all six today, two are + browser applications, and the other five are listed in [Status]({{< relref "/status.md#what-ci-does-not-cover-yet" >}}). +- A statement-coverage gate in CI: 100% in eighteen of the nineteen. Where a + file cannot run in a test (a browser loop, a native run loop), the module + says which file is left out and why. See [Status]({{< relref "/status.md" >}}) + for the exceptions. +- BSD-3-Clause. diff --git a/content/en/rendering/_index.md b/content/en/rendering/_index.md new file mode 100644 index 0000000..c61c18d --- /dev/null +++ b/content/en/rendering/_index.md @@ -0,0 +1,29 @@ +--- +title: "How a widget reaches the screen" +linkTitle: "Rendering" +weight: 10 +description: "A widget draws through a painter; the painter decides whether that becomes pixels or terminal cells; a back-end presents the result." +tags: [rendering, painter, toolkit] +--- + +Three layers, and each knows only the one below it: + +```text +toolkit widget tree Button, Table, Agenda, VBox ... (or a skin.Object) + │ Draw(p painter.Painter, theme *Theme) + ▼ +painter.Painter FillRect, StrokeRect, Text, PutPixel, ... + │ + ├─ PixelPainter → an RGBA []byte → window, browser canvas, PNG, Android + └─ CellPainter → a grid of cells → a terminal, as 24-bit ANSI +``` + +A widget never learns which painter it was handed. A back-end never learns +which widgets drew. That is the whole reason one widget tree can be shown in a +native window, a browser and a terminal without a line of conditional code. + +| | | +|---|---| +| [The painter seam]({{< relref "/rendering/painter.md" >}}) | the primitives, the optional capabilities, the two painters | +| [The toolkit]({{< relref "/rendering/toolkit.md" >}}) | the widget set, themes, layouts, text and accessibility | +| [Skins]({{< relref "/rendering/skin.md" >}}) | describe a widget's parts and states as data instead of Go | diff --git a/content/en/rendering/painter.md b/content/en/rendering/painter.md new file mode 100644 index 0000000..89343b1 --- /dev/null +++ b/content/en/rendering/painter.md @@ -0,0 +1,71 @@ +--- +title: "The painter seam" +linkTitle: "Painter" +weight: 10 +description: "The primitives every widget is drawn with, the optional capabilities a painter may add, and the pixel and cell painters." +tags: [rendering, painter] +--- + +[`go-widgets/painter`](https://github.com/go-widgets/painter) defines the one +interface every widget draws through: + +```go +type Painter interface { + FillRect(r Rect, c RGBA) + StrokeRect(r Rect, c RGBA, lineW int) + FillRoundRect(r Rect, radius int, c RGBA) + StrokeRoundRect(r Rect, radius int, c RGBA, lineW int) + PutPixel(x, y int, c RGBA) + Text(x, y int, s string, ink RGBA) + Size() (w, h int) +} +``` + +The set is small on purpose: every back-end must implement all of it, and a +terminal grid cannot round a corner or vary a stroke width. So the contract +says what happens when it cannot — `StrokeRect`'s width is a hint a cell grid +ignores, and `FillRoundRect` falls back to a square fill there. + +## Optional capabilities + +What not every surface can do is not in the base interface. A widget that +needs it type-asserts, and draws something sensible when the answer is no: + +| Interface | Methods | Used for | +|---|---|---| +| `Clipper` | `PushClip`, `PopClip` | keeping a scrolled child inside its parent's bounds | +| `Translator` | `PushTranslate`, `PopTranslate` | drawing a child in its own coordinates | +| `PathPainter` | `FillPath`, `StrokePath` | curves, charts, anti-aliased shapes | +| `ImagePainter` | `DrawImage` | icons, thumbnails, pictures | +| `MaskPainter` | `DrawMask` | glyphs and other coverage masks in one colour | +| `FacePainter` | `TextFace` | text in a specific font face and size | + +```go +if c, ok := p.(painter.Clipper); ok { + c.PushClip(bounds) + defer c.PopClip() +} +``` + +## The two painters + +| | Writes into | Presented by | +|---|---|---| +| `NewPixelPainter(buf, w, h)` | a caller-owned RGBA `[]byte` (`NewPixelPainterBGRA` for BGRA surfaces) | a native window, a browser canvas, an Android surface, a PNG or SVG | +| `NewCellPainter(w, h)` | a grid of cells (rune, foreground, background) with a 24-bit ANSI serialiser | a terminal, through [`tui`]({{< relref "/surfaces/terminal.md" >}}) | + +The buffer stays the caller's. A `PixelPainter` only translates primitives +into writes, so the same `[]byte` can be handed to a window back-end, a +`<canvas>` or an image encoder without a copy in between. + +## Trying it + +The repository's own demos draw the same three widgets through both painters: + +```sh +go run ./cmd/wui-demo --out demo.png # pixels, written as a PNG +go run ./cmd/tui-demo --theme dark # cells, written as ANSI to stdout +``` + +and [go-widgets.github.io/painter](https://go-widgets.github.io/painter/) shows +the pixel painter in a browser. diff --git a/content/en/rendering/skin.md b/content/en/rendering/skin.md new file mode 100644 index 0000000..9dd0865 --- /dev/null +++ b/content/en/rendering/skin.md @@ -0,0 +1,81 @@ +--- +title: "Skins: a widget's parts and states as data" +linkTitle: "Skins" +weight: 30 +description: "go-widgets/skin describes a widget's parts, per-state visuals and signal-driven transitions in a .skin.json file, rendered by one engine." +tags: [rendering, skin, theme] +--- + +The toolkit's widgets are written in Go: a `Button`'s parts, its hover, +pressed and disabled faces and its click feedback all live in `button.go`. +That is fast and exact, and every visual change is a code change. + +[`go-widgets/skin`](https://github.com/go-widgets/skin) moves that description +into data — the idea of Enlightenment's Edje. A `.skin.json` document names a +widget's **parts**, their look in each **state**, and the **programs** that +move them between states when a signal arrives. Re-skinning, or building a new +control, then needs no recompilation: the program loads different bytes. + +```go +theme, err := skin.Load(jsonBytes) // parse and validate every collection +if err != nil { + return err +} +obj, _ := theme.New("button") +obj.Bind(skin.NewMVVMContext().Set("$.label", labelObs)) +obj.SetBounds(toolkit.Rect{W: 96, H: 28}) + +// each frame +obj.OnEvent(ev) // pointer events become signals, signals states +busy := obj.Tick(time.Now()) // advance the transitions +obj.Draw(p, theme.Palette()) +``` + +A `skin.Object` is a `toolkit.Widget`, so it sits in a `VBox` next to +hand-written widgets. The layouts place widgets; a skin arranges what is +inside one. + +## The format + +```jsonc +{ + "collections": { + "button": { + "min": { "w": 96, "h": 28 }, + "parts": [ + { "name": "bg", "type": "rect", + "states": { + "default": { "color": "@surface", "border": "@border", "radius": 6 }, + "hover": { "color": "@surface_alt", "border": "@border", "radius": 6 }, + "pressed": { "color": "@accent", "border": "@border", "radius": 6 } } }, + { "name": "label", "type": "text", "text_from": "$.label", "align": [0.5, 0.5], + "states": { "default": { "ink": "@on_surface" } } } + ], + "programs": [ + { "on": "mouse,in", "target": ["bg", "label"], "to": "hover", "in": 0.12, "ease": "ease_out_cubic" }, + { "on": "mouse,click", "emit": "clicked" } + ] + } + } +} +``` + +- A **part** is a `rect`, `text` or `image`, placed by two relative corners + (`rel1`, `rel2`) against the object or an earlier part, with pixel and + font-relative (`offset_em`) offsets, so a text part follows the font. +- A **state** sets colours, border, radius and visibility, and may move or + resize the part. +- A **program** reacts to a signal, moves parts to a state over a duration + with a named easing, and may `emit` a signal outwards — to an + [`mvvm.Command`]({{< relref "/state/mvvm.md" >}}), for instance. +- Colours are `@tokens` resolved against the toolkit `Theme` (including every + GTK `@define-color` entry as `@extra:<name>`), hex, or RGBA arrays. + +JSON is decoded by the standard library: no third-party dependency. + +## Proven against the hand-written widgets + +The toolkit's button, check box, switch, chip and card are each written again +as a skin collection, and the tests assert that the two render **pixel for +pixel identically** in every state. A skinned `Draw` is measured against the +hand-written one in a benchmark. diff --git a/content/en/rendering/toolkit.md b/content/en/rendering/toolkit.md new file mode 100644 index 0000000..9e3746c --- /dev/null +++ b/content/en/rendering/toolkit.md @@ -0,0 +1,74 @@ +--- +title: "The toolkit" +linkTitle: "Toolkit" +weight: 20 +description: "160 widget types drawn through the painter seam: themes, layouts, text, input, accessibility." +tags: [rendering, toolkit, widgets, theme, accessibility] +--- + +[`go-widgets/toolkit`](https://github.com/go-widgets/toolkit) is the widget +set. Every widget implements the same interface — `Bounds`, `SetBounds`, +`Draw(p painter.Painter, theme *Theme)`, `HitTest`, `OnEvent` — so a host +treats a button, a table and an event calendar alike. Standard library only, +`CGO_ENABLED=0`, 100% statement coverage. + +At v0.328.0 it has **160 exported widget types** in about **69,000 lines** of +widget code. Those two numbers are measured at that tag, not carried over: the +toolkit's README gives the commands, and they should be run again rather than +the figures repeated. + +## The families + +| Family | Widgets | +|---|---| +| Action | `Button`, `ToggleButton`, `CheckButton`, `RadioButton` + `RadioGroup`, `Switch`, `SplitButton`, `IconButton`, `CycleButton`, `Chip`, `SegmentedBar` | +| Input | `Entry`, `TextView` (selection, IME preview, syntax spans, line numbers), `SpinButton`, `Scale`, `RangeSlider`, `SearchEntry`, `TagField`, `ComboBox`, `FormField` with `Validate`/`Rule` | +| Selection | `ListBox`, `TreeView`, `DropDown` | +| Containers | `Container` with a swappable `Layout` (`FitLayout`, `BoxLayout`, `BorderLayout`, `CardLayout`, `FlowLayout`); `HBox`, `VBox`, `Grid`, `Frame`, `Dock`, `Border`, `Stack`, `Overlay`, `Paned`, `Expander`, `Accordion`; a declarative `Node` builder | +| Navigation | `Menu`, `MenuBar` (with `Alt`+letter), `ContextMenu`, `Popover`, `CommandPalette`, `Dialog`, `MessageDialog`, `Wizard`, `Notebook`, `ViewSwitcher`, `Carousel` | +| Feedback | `ProgressBar`, `ProgressCircle`, `LevelBar`, `Spinner`, `Tooltip`, `Notification`, `Toast`, `Banner`, `Alert`, `Badge`, `Skeleton` | +| Data | `Table` (cell editing, frozen columns, group rows, aggregates, row expanders), `TreeTable`, `PropertyGrid`, `Kanban`, `Gantt`, `Agenda` (week, month, quarter, year; several calendars) | +| Charts | `LineChart`, `BarChart`, `PieChart`, `AreaChart`, `ScatterChart`, `RadarChart`, `Gauge`, `Sparkline` | +| Composite | `FileChooser`, `ColorChooser`, `ColorPicker`, `FontChooser`, `Calendar`, `DatePicker`, `DateRangePicker`, `TimePicker`, `MarkdownView`, `MarkdownEditor`, `TerminalView` | +| Shell | `Window` with client-side decorations, `HeaderBar`, `Toolbar`, `Statusbar`, `StatusIcon`, `StatusArea`, `Wallpaper`, `Thumbnail` | + +Every one of them, live: the [gallery](https://go-widgets.github.io/gallery/). + +## Themes + +One `Theme` value cascades through the whole tree: change a colour at the +root and every widget repaints with it. `DefaultLight()` and `DefaultDark()` +are built in, and `LoadGTKTheme(css)` reads the `@define-color` block of any +GTK 3 or libadwaita theme — Adwaita, WhiteSur, Solarized — into a `Theme`. + +## Text + +The compiled-in default is a 5×7 bitmap font, so pixel tests keep their +geometry. One call switches the whole UI to anti-aliased, shaped text: + +```go +toolkit.UseOpenTypeText() // Atkinson Hyperlegible, bundled; js/wasm-safe +``` + +The face comes from [go-opentype/fonts](https://github.com/go-opentype/fonts) +and is rasterised by [go-opentype](https://github.com/go-opentype/opentype), +in Go, with no C font library and no system font lookup. +`NewTrueTypeFont(ttf, px)` loads another face; `NewFallbackFont` chains +several, for example to add a CJK or Arabic face; a widget's own `Font` field +overrides the global one for that widget alone. + +## Input + +Mouse, keyboard, IME composition, touch (`EventTouchStart`/`Move`/`End`, with +`GestureRecognizer` for taps, long presses and swipes), drag and drop +(`DragSource`, `DropTarget`). Multi-row selection in `ListBox`, `Table` and +`TreeView` works from the mouse and from the keyboard, and honours ⌘ as well +as Ctrl, because on macOS Ctrl-click is the secondary click. + +## Accessibility + +A toolkit that paints its own pixels gives a screen reader nothing to read +unless it says what it drew. Widgets implement `Accessible` — a role, a name, +a value — and `CollectA11y` gathers them for the host to publish: AT-SPI on +Linux through [`window`]({{< relref "/surfaces/native-window.md" >}}), and a +virtual view hierarchy on [Android]({{< relref "/surfaces/android.md#accessibility" >}}). diff --git a/content/en/state/_index.md b/content/en/state/_index.md new file mode 100644 index 0000000..dfb0445 --- /dev/null +++ b/content/en/state/_index.md @@ -0,0 +1,23 @@ +--- +title: "State lives in a view-model" +linkTitle: "State" +weight: 20 +description: "Why application state is held in observables and bound to widgets, rather than written into widget fields." +tags: [state, mvvm] +--- + +A widget's fields — an entry's text, a list's items, a table's rows — are its +**view** of the state, not the state. An application that assigns them from +wherever it happens to be scatters its state across call sites, and two of +those call sites eventually disagree about what is on screen. + +So state lives in a **view-model**: plain Go values wrapped in observables, +with no widget in sight, testable without a window. The view binds each widget +to the view-model once, and from then on the binding — not the application — +keeps them in step, in both directions. + +| | | +|---|---| +| [MVVM]({{< relref "/state/mvvm.md" >}}) | `Observable`, `Command`, `ObservableList`; binders for the toolkit and the terminal widgets; undo and redo | +| [The data spine]({{< relref "/state/data.md" >}}) | typed records with validation, queries, and a store that runs the same pipeline locally or over gRPC | +| [`mvvmlint`]({{< relref "/guard-rails/mvvmlint.md" >}}) | the CI check that fails a pull request assigning a widget's state field by hand | diff --git a/content/en/state/data.md b/content/en/state/data.md new file mode 100644 index 0000000..1d83f9d --- /dev/null +++ b/content/en/state/data.md @@ -0,0 +1,45 @@ +--- +title: "The data spine" +linkTitle: "Data" +weight: 20 +description: "go-widgets/data: typed records with validation, a pure query engine, and a store that runs the same pipeline in-process or over gRPC, natively and in wasm." +tags: [state, data, grpc] +--- + +[`go-widgets/data`](https://github.com/go-widgets/data) is headless: it +imports the standard library and [`mvvm`]({{< relref "/state/mvvm.md" >}}), +and no widget. + +| Piece | What it is | +|---|---| +| `Value`, `Kind` | a comparable typed scalar: string, int, float, bool | +| `Record`, `Schema`, `Field`, `Rule` | a typed row and its validation | +| `Query` → `Apply` → `View` | the query engine: filter, sort, group, page, aggregate | +| `Proxy` | the back-end seam: `List`, `Query`, `Mutate` | +| `MemoryProxy` | the in-process back-end | +| `grpcproxy.Server`, `grpcproxy.Client` | the same contract over gRPC, carried over a WebSocket | +| `Store[R]` | a typed collection, published as an `mvvm.ObservableList` | + +## The same answer, local or remote + +`Store` talks to a `Proxy` and never knows which one. `Apply` is a pure +function of the records and the query, so the client and the server run the +same code on the same rows — and a `MemoryProxy` and a `grpcproxy.Client` +return byte-identical views. The conformance test checks exactly that: a +battery of sort, filter, group, page and aggregate queries through both +proxies and through the engine, every result canonicalised, all three +compared byte for byte. + +The WebSocket transport compiles to `js/wasm`, so a browser application +reaches the same service through the same client. + +```go +store := data.NewStore(mem, codec) // or a grpcproxy.Client: nothing else changes +store.SetQuery(data.Query{Sorts: []data.Sort{{Field: "salary", Desc: true}}, Limit: 20}) +store.Load(ctx) +items := store.Items() // bind this to a view +``` + +`tkbind.BindTable` puts a store behind a toolkit `Table`: a header click +sorts, a group-by groups, and a validated cell edit becomes a mutation through +the proxy — the same round trip, local or remote. diff --git a/content/en/state/mvvm.md b/content/en/state/mvvm.md new file mode 100644 index 0000000..9e0b23e --- /dev/null +++ b/content/en/state/mvvm.md @@ -0,0 +1,95 @@ +--- +title: "MVVM: observables, commands, binders" +linkTitle: "MVVM" +weight: 10 +description: "go-widgets/mvvm, its toolkit and terminal binders, mvvmtk's one-call helpers, and an undo stack." +tags: [state, mvvm, mvvmtk, undo] +--- + +[`go-widgets/mvvm`](https://github.com/go-widgets/mvvm) is generics only, with +no reflection. Its core package imports no widget package at all, so one +view-model drives the pixel [toolkit]({{< relref "/rendering/toolkit.md" >}}) +and the [terminal widgets]({{< relref "/surfaces/terminal.md" >}}) alike. + +| Primitive | Role | +|---|---| +| `Observable[T]` | a property: `Get`, `Set`, `Subscribe`. Setting an equal value does nothing. | +| `Command` | an action, with `CanExecute` and `RaiseCanExecuteChanged` | +| `ObservableList[T]` | a collection that reports each insert, remove, replace, move and reset | + +## A form + +```go +type FormVM struct { + Name *mvvm.Observable[string] + Names *mvvm.ObservableList[string] + Save *mvvm.Command +} + +vm := &FormVM{Name: mvvm.NewObservable(""), Names: mvvm.NewObservableList[string]()} +vm.Save = mvvm.NewCommand( + func() { vm.Names.Append(vm.Name.Get()); vm.Name.Set("") }, + func() bool { return vm.Name.Get() != "" }, // CanExecute +) +mvvm.BindCanExecute(vm.Save, vm.Name) // Save greys out while the name is empty +``` + +## Binding + +The generic binders reach a widget through a **pointer to its value field and +a pointer to its callback slot**: `BindField` (two-way), `OneWay`, +`BindCommand`, `BindList`. A widget fires its callback from its event handler +and leaves a direct field write silent, and `Observable.Set` ignores an equal +value — so a two-way binding can echo without recursing. + +The toolkit is moving its widgets to exposing their state **as** an +observable (`Label.Text()` returns an `*mvvm.Observable[string]`). There is +then no field to seed and no callback to compose, only two properties that +must agree: `BindTwoWay(src, dst, invalidate)`. + +Widgets whose callback takes several arguments get a small named adapter in a +per-back-end package, the only packages that import a back-end: + +| Package | Adapters | +|---|---| +| `mvvm/tkbind` | `BindRange` (the two-handle `RangeSlider`), `BindContainer` and `BindCardActive` (a container's children and a card layout from a list or an index), `BindStore` and `BindTable` (a `Table` over a [data store]({{< relref "/state/data.md" >}}): sorting, grouping and validated inline edits) | +| `mvvm/tuibind` | `BindDropdown`, `BindTableSelection` | + +## mvvmtk: one call per widget + +[`go-widgets/mvvmtk`](https://github.com/go-widgets/mvvmtk) fills in each +toolkit widget's real field and callback names, so the view never names them: + +```go +unbind := mvvmtk.BindText(search, vm.Query, win.Invalidate) // search.Text ⇄ vm.Query +defer unbind() +``` + +| Helper | Direction | +|---|---| +| `BindText` (`SearchEntry`), `BindEntryText`, `BindChecked`, `BindSelectedIndex`, `BindSpin`, `BindListSelection`, `BindViewSwitcher` | two-way | +| `BindLabel`, `BindProgress` | view-model → widget | +| `BindListItems`, `BindDropDownOptions`, `BindViews`, `BindTree` | list → widget | +| `BindCommand` | a button's click, its `Disabled` state and its greying follow the command | + +Every helper returns an `unbind` that detaches the binding and restores the +callback that was there before. A button bound to a command that cannot run +takes no click, no Enter or Space, and no keyboard focus. + +## Undo and redo + +`mvvm/undo` is a command stack built on the core primitives alone, so it +works under any view: + +```go +s := undo.New() // unlimited, coalescing on; undo.WithLimit(n), undo.WithCoalescing(false) +s.Push(undo.NewCommand("Write hello", do, undoIt)) +s.Undo() +s.Redo() + +mvvm.BindCommand(s.UndoCommand(), &undoBtn.OnClick, setEnabled) +mvvm.OneWay(s.UndoTextBinding(), &undoBtn.Text, repaint) // "Undo Write hello" +``` + +A run of keystrokes coalesces into one step, and a push after an undo drops +the redo tail. diff --git a/content/en/status.md b/content/en/status.md new file mode 100644 index 0000000..2d686e9 --- /dev/null +++ b/content/en/status.md @@ -0,0 +1,64 @@ +--- +title: "Status" +weight: 60 +description: "What has been proven on a real display, a real device or a real terminal; what is only compiled; and what CI does not cover yet." +tags: [status] +--- + +These pages were checked against the modules at the tags listed under +[Modules]({{< relref "/modules.md" >}}). Where a module's README and its code +disagree, the pages follow the code. + +## Proven live + +A codec tested in-process proves the codec. These are the places where the +platform itself was asked: + +| What | Where it was proven | +|---|---| +| X11 window | CI, under Xvfb: a known pattern presented, captured and sampled; a click and a key synthesised with `xdotool` and the dispatched `toolkit.Event` asserted; `Show`/`Hide`/`Raise` read back from the server | +| Cocoa window | a macOS CI lane: a window opened and rendered, pixels sampled, a click and a key synthesised; `Show`/`Hide`/`Raise` asked of a real `NSWindow` from another goroutine and read back from AppKit | +| Win32 window | a Windows 11 arm64 VM: a real window rendering a `VBox`, a `Label` and a `Button`, three injected clicks driving its counter from 0 to 3 through the window procedure | +| Wayland window | against a fake compositor that enforces xdg-shell's map and unmap rules | +| wasmbox window | headless Chromium driving the real wasmdesk compositor: the composited pixels read back, a real click routed to the widget | +| Android | an Android 15 arm64 device: damage-only blits measured, memfd dirty memory measured, the accessibility tree benchmarked on the device | +| macOS tray | a real macOS session: the icon leaves the menu bar when the program stops, returns when it starts, and opens its menu | +| Linux tray | CI against a real session bus: the name is claimed, the menu answers `GetLayout`, `Quit` withdraws it | +| Windows tray | the icon and its menu on a Windows 11 VM | +| Terminal demos | a real pty in CI: key bytes sent, the rendered frame asserted | + +## Compiled, not run + +- Win32 `Show`/`Hide`/`Raise`: the decision table is unit-tested, the calls + on the window's thread are only compiled. +- The Windows tray's `Attach`. +- `webcanvas`'s DOM loop (`run_js.go`): behind a `js && wasm` build tag, so it + is outside the measured coverage; the applications that use it exercise it. +- `application`'s native `Run`: opening a real window is excluded from its + coverage gate; the contract, the event translation and the ready counter + are covered. + +## Not done + +- The Windows and Linux trays ignore `MenuItem.Icon`: a row with an icon + draws without it. +- `window`'s Wayland back-end cannot `Raise`: xdg-shell has no such request, + and xdg-activation needs a token from the user's own input. +- Android: `android/arm64` only, because it is the only Android target Go + links without cgo. +- `window.Screens` returns `ErrScreensUnsupported` on `js/wasm`. + +## What CI does not cover yet + +Measured from each repository's workflows at the tags above: + +| Module | Gap | +|---|---| +| `mvvmtk` | no coverage gate; cross-builds amd64 and arm64 only, on six operating systems | +| `application` | cross-builds Linux, macOS and Windows on amd64 only | +| `tray` | cross-builds four OS/architecture pairs | +| `mvvmlint` | no cross-build | +| `bricolint` | its "6 arches" step builds five: loong64 is missing | + +`app-template` and `gallery` are browser applications and build for `js/wasm`, +which is their only target. diff --git a/content/en/surfaces/_index.md b/content/en/surfaces/_index.md new file mode 100644 index 0000000..ca9acd6 --- /dev/null +++ b/content/en/surfaces/_index.md @@ -0,0 +1,31 @@ +--- +title: "Where it paints" +linkTitle: "Surfaces" +weight: 30 +description: "Every surface a go-widgets tree can be shown on, and what chooses between them." +tags: [surfaces, back-ends] +--- + +| Surface | Module | Painter | +|---|---|---| +| [A native window]({{< relref "/surfaces/native-window.md" >}}) on X11, Wayland, macOS, Windows | `window` | pixels | +| [An application]({{< relref "/surfaces/application.md" >}}) with a tray icon and the system appearance | `application`, `tray` | pixels | +| [A browser tab]({{< relref "/surfaces/browser.md" >}}): a plain `<canvas>` | `webcanvas` | pixels | +| [A browser desktop]({{< relref "/surfaces/browser.md#a-window-on-the-wasmdesk-desktop" >}}): a window of the wasmdesk compositor | `window` (wasmbox) | pixels | +| [A terminal]({{< relref "/surfaces/terminal.md" >}}) | `tui` | cells | +| [An Android APK]({{< relref "/surfaces/android.md" >}}) | `android`, `window` | pixels | +| [A picture]({{< relref "/surfaces/snapshots.md" >}}): SVG or PNG | `svg` | pixels | + +The widget tree does not change between them. What changes is who owns the +buffer and who delivers input — a display server, a browser, a terminal, a +Java host — and each back-end turns that input into the same `toolkit.Event`. + +## Chosen by what is there + +`window.Open` takes no back-end argument. It looks at the environment — the +platform it was built for, `$WAYLAND_DISPLAY`, `$DISPLAY`, the socket an +Android host exports — and picks the one that can work. A binary built for +Linux opens a Wayland window under a Wayland session and an X11 window under +X; the same Android binary dials its host inside an APK and opens an ordinary +Linux window under Termux. See +[how `Open` chooses]({{< relref "/surfaces/native-window.md#how-open-chooses" >}}). diff --git a/content/en/surfaces/android.md b/content/en/surfaces/android.md new file mode 100644 index 0000000..ae9aa7c --- /dev/null +++ b/content/en/surfaces/android.md @@ -0,0 +1,64 @@ +--- +title: "Android" +linkTitle: "Android" +weight: 50 +description: "go-widgets/android: an installable APK whose whole interface is laid out and painted by a CGO_ENABLED=0 Go process." +tags: [surfaces, android] +--- + +Android is the one platform where a CGO-free process cannot own a window: every +path to a drawable surface goes through JNI, and JNI needs cgo. There is no +wire protocol to speak instead, the way X11 and Wayland have one. + +So [`go-widgets/android`](https://github.com/go-widgets/android) is two +processes: + +| | | +|---|---| +| **Java host** (`host/`) | owns the `Activity`, the `SurfaceView`, touch, keys and the lifecycle. Blits pixels; knows nothing about widgets. | +| **Go application** (`cmd/gwapp`) | an ordinary `CGO_ENABLED=0 GOOS=android` executable. Owns layout, widgets, theme, hit-testing and focus, unchanged from every other back-end. | + +The Java host stands where the X server stands on Linux. Pixels travel through +a **memfd** the application creates and hands to the host over the socket; +the Go side writes RGBA_8888, which is byte for byte Android's ARGB_8888, so +the blit is a copy with no conversion, of the damaged rectangle only. Input, +insets, IME text and the accessibility tree cross a framed protocol over an +abstract `LocalSocket`. + +```go +c, err := android.Dial("my app", nil) // nil theme: toolkit.DefaultDark() +if errors.Is(err, android.ErrUnsupported) { + return nil // not under a host +} +defer c.Close() +return c.Run(myWidgetTree()) +``` + +`Client` satisfies `window`'s `Backend`, so an application moves between +Android and the desktop back-ends without changing a line above the window — +and [`window.Open`]({{< relref "/surfaces/native-window.md#how-open-chooses" >}}) +picks it by itself when the host's socket is there. + +## arm64 only + +`android/arm64` is the only Android target Go links without cgo; `arm`, +`amd64` and `386` require external linking. CI asserts both halves — the one +that builds and the three that do not — so the day Go lifts the restriction, +the build says so. + +## Accessibility + +A screen reader would see the `SurfaceView` as one opaque rectangle. The host +gives it a virtual view hierarchy instead: one node per accessible widget, +with the `android.widget.*` class Android chooses its announcements from, the +text, the bounds, and an activation action. The tree is **pulled** when +something reads it, never pushed, so an app with no accessibility service +attached never builds one. An activation comes back as an ordinary click at +the element's centre, through the same code a touch takes. + +## Touch and animation + +Each touch sample reaches the tree as a touch event, then a mouse event: the +first for gesture-aware widgets, the second for the many that only listen for +clicks. Animated widgets advance on a frame loop that starts when something +begins animating and stops when nothing does. diff --git a/content/en/surfaces/application.md b/content/en/surfaces/application.md new file mode 100644 index 0000000..2ba4b5d --- /dev/null +++ b/content/en/surfaces/application.md @@ -0,0 +1,82 @@ +--- +title: "An application and its tray" +linkTitle: "Application and tray" +weight: 20 +description: "go-widgets/application owns the lifecycle above a window; go-widgets/tray puts an icon and a menu in the menu bar or notification area." +tags: [surfaces, application, tray] +--- + +## The application + +[`go-widgets/application`](https://github.com/go-widgets/application) is the +layer above [`window`]({{< relref "/surfaces/native-window.md" >}}): it opens +the window, drives its loop, attaches a tray icon once the first frame is up, +passes the live system appearance (dark or light, accent colour, system font) +to the application, publishes the accessibility tree, and calls `onReady` +once the first frame is on screen. + +```go +spec := application.Spec{ + Name: "News Reader", + Identifier: "com.example.reader", + Version: "1.0.0", + Tray: func() *tray.Menu { + return tray.NewMenu().Add( + tray.Item("Refresh", refresh), + tray.Item("Quit", quit), + ) + }, +} +cfg := application.Config{Title: "News Reader", Width: 1200, Height: 800} +err := application.Run(spec, cfg, handler, func() { log.Println("on screen") }) +``` + +`handler` implements `Handler` — `Frame`, `Resize`, mouse, scroll and key +events — and may implement any of the optional capabilities: appearance, +shortcuts, modified clicks and keys, secondary click, context menus, +clipboard, accessibility, native controls. The loop never wraps the handler; +it type-asserts each capability and honours the ones it finds. A test walks +the package for those assertions and fails if the package documentation does +not name every one. + +A tray that cannot be attached is `Run`'s error, not a silence: +`errors.Is(err, tray.ErrNoBackend)` says why. An application that wants its +window whatever happens to the tray leaves `Spec.Tray` nil and runs one +itself. + +`Run` closes its window when the loop returns, so a tray application's +"Open" can call `Run` again for a fresh window. For a host that is not a +window — a desktop shell, a browser tab — `Bind` and `BindScaled` return a +`*toolkit.Surface` wired to the same handler. + +## The tray + +A tray icon is OS integration, not a painted widget, so +[`go-widgets/tray`](https://github.com/go-widgets/tray) drives each platform's +own API, still with `CGO_ENABLED=0`: + +| Platform | API | Through | +|---|---|---| +| macOS | `NSStatusItem`, `NSMenu` | purego and the Objective-C runtime | +| Windows | `Shell_NotifyIcon`, `TrackPopupMenu` | `golang.org/x/sys/windows` | +| Linux | `StatusNotifierItem`, `com.canonical.dbusmenu` | DBus, in Go | + +```go +menu := tray.NewMenu().Add( + tray.Item("Open", open), + tray.Checkbox("Notifications", true, setNotify), + tray.SubMenu("Recent", tray.NewMenu().Add(tray.Item("file.txt", nil))), + tray.Separator(), + tray.Item("Quit", func() { t.Quit() }), +) +t := tray.New(iconPNG).SetTooltip("My App").SetMenu(menu) +t.Run() // blocks on the platform's loop until Quit +``` + +A program already running a window's loop cannot give `Run` its main thread: +`Attach` puts the same icon up and returns, and `Quit` takes it down. + +The native back-ends are on by default; there is no build tag to remember. +On a platform with none, `Run` returns `ErrNoBackend` — the difference +between "there is no tray here" and a tray that silently does nothing. A test +or a headless service passes `WithBackend(tray.NewHeadless())`. diff --git a/content/en/surfaces/browser.md b/content/en/surfaces/browser.md new file mode 100644 index 0000000..eea084c --- /dev/null +++ b/content/en/surfaces/browser.md @@ -0,0 +1,54 @@ +--- +title: "A browser tab" +linkTitle: "Browser" +weight: 30 +description: "Run a widget tree in a plain <canvas> with go-widgets/webcanvas, or as a window of the wasmdesk compositor." +tags: [surfaces, wasm, browser, webcanvas, wasmdesk] +--- + +The toolkit is pure Go with no DOM dependency, so `GOOS=js GOARCH=wasm` builds +it like any other target. Two ways to put the result in front of somebody: + +## A plain canvas + +[`go-widgets/webcanvas`](https://github.com/go-widgets/webcanvas) blits an RGBA +framebuffer into a `<canvas>` and routes DOM pointer and keyboard events back +into the scene. No compositor, no `SharedArrayBuffer`, no cross-origin +isolation headers: any static host serves it. + +```go +//go:build js && wasm + +package main + +import "github.com/go-widgets/webcanvas" + +func main() { webcanvas.Run("screen", myScene()) } +``` + +The scene implements `App`: `Size`, `Draw(buf []byte)`, and one method per +kind of event, each reporting whether anything changed so an unchanged frame +is not repainted. It may also implement `Ticker`, `Animator`, `Resizer` or +`Scroller`. + +The [gallery](https://go-widgets.github.io/gallery/) and +[`app-template`]({{< relref "/getting-started.md" >}}) run this way. + +## A window on the wasmdesk desktop + +On `js/wasm`, [`window.Open`]({{< relref "/surfaces/native-window.md" >}}) +returns a client of the [wasmdesk/wasmbox](https://github.com/wasmdesk/wasmbox) +browser compositor. It allocates its surface in a `SharedArrayBuffer`, says +`hello` over its `MessagePort`, waits for `welcome`, paints into the shared +buffer and posts `commit` — the whole surface, or only the damaged rectangles. +The compositor's `input` messages become `toolkit.Event`, as on X11. + +So an application written against `window` runs unchanged as a native window +and as a window of a desktop in a browser tab. The +[desktop shell demo](https://github.com/go-widgets/desktop) does exactly +that, from one source. + +The protocol codec is covered at 100% on every platform. The live proof runs +headless Chromium against the real wasmdesk compositor: it spawns the client, +reads the compositor's own composited pixels, clicks, and checks that the +click reached the widget. diff --git a/content/en/surfaces/native-window.md b/content/en/surfaces/native-window.md new file mode 100644 index 0000000..310e46f --- /dev/null +++ b/content/en/surfaces/native-window.md @@ -0,0 +1,90 @@ +--- +title: "A native window" +linkTitle: "Native window" +weight: 10 +description: "go-widgets/window: one Open/Run API over X11, Wayland, Cocoa, Win32, GTK4, Android and wasmbox, all with CGO_ENABLED=0." +tags: [surfaces, window, x11, wayland, macos, windows] +--- + +[`go-widgets/window`](https://github.com/go-widgets/window) opens a real window +and runs a widget tree in it: layout, draw, present, translate input into +`toolkit.Event`, repeat until the window closes. + +```go +w, err := window.Open(window.Config{Title: "Demo"}) +if err != nil { + return err +} +defer w.Close() +return w.Run(root) // root is any toolkit.Widget +``` + +`Config` also takes the initial size in **logical points** (zero asks for a +readable default), the `WM_CLASS` instance and class, a `Theme`, and a +`RenderScale`. + +## How Open chooses + +| Platform and environment | Back-end | How it reaches the screen | +|---|---|---| +| Linux, `$GO_WIDGETS_GTK` set | GTK4 | GTK owns the window; the framebuffer is a `GtkPicture`, and native controls are real GTK widgets above it. Needs the libgtk-4 runtime. | +| Linux, `$WAYLAND_DISPLAY` set | Wayland | xdg-shell over the compositor's unix socket | +| Linux, otherwise `$DISPLAY` | X11 | the X11 core protocol over the unix socket, with MIT-SHM | +| macOS | Cocoa | `NSWindow` and `NSView` through [go-macos/objc](https://github.com/go-macos/objc) (purego) | +| Windows | Win32 | a top-level `HWND` through user32 and gdi32 syscalls, `StretchDIBits` | +| Android, `$GW_ANDROID_SOCKET` set | Android host | a framed protocol to [the Java host]({{< relref "/surfaces/android.md" >}}), pixels in a shared memfd | +| Android, otherwise | Wayland or X11 | a shell under Termux still has a display server to dial | +| `js/wasm` | wasmbox | a client of the [wasmdesk compositor]({{< relref "/surfaces/browser.md#a-window-on-the-wasmdesk-desktop" >}}) | +| anything else | — | `window.ErrUnsupported`, so a cross-build still compiles | + +Every back-end is `CGO_ENABLED=0`. The X11 one is the core protocol written +from scratch over the socket — no Xlib, no XCB — with both byte orders, the +Xauthority cookie, keysym mapping, `PutImage` tiled under the server's +request size, and the MIT-SHM fast path, the shared segment passed over +`SCM_RIGHTS`. Windows is reached through the process's own DLLs with +`syscall.NewLazyDLL` and a `syscall.NewCallback` window procedure; macOS +through the Objective-C runtime with purego. + +The platform-independent part of each back-end — event mapping, coordinate +maths, pixel packing, damage rectangles — lives in a codec covered at 100%, +on every operating system. The thin platform glue is proven live: see +[Status]({{< relref "/status.md" >}}). + +## Only what changed + +A root that implements `DamageRenderer` (as `toolkit/scene.HostRoot` does) +reports the rectangles it repainted, and the Cocoa, Win32 and wasmbox +back-ends present only those. + +## HiDPI + +The default is one framebuffer pixel per logical point: the UI is laid out +and painted at a readable size, and the compositor up-samples it. On Windows +the window declares per-monitor DPI awareness and the OS scales the logical +frame to the physical client area. `RenderScale` asks for a framebuffer at +the panel's real resolution, which is correct only for a root that lays out +in device pixels. + +## Screens, and showing and hiding + +`window.Screens()` lists the attached displays, primary first, in logical +points with the desktop's panels excluded, and may be called before `Open`. +A screen's name is the panel's own (`"DELL U2720Q"`, from its EDID), falling +back to the connector (`"HDMI-1"`) when it publishes none — and also when two +attached panels publish the same one, since a name that cannot tell them +apart is not a name. + +`window.Show`, `Hide` and `Raise` take an open window off the screen without +closing it and bring it back — what a [tray application]({{< relref "/surfaces/application.md#the-tray" >}}) +needs. + +| Back-end | Show | Hide | Raise | +|---|---|---|---| +| X11 | `MapWindow` | ICCCM withdraw, so it leaves the taskbar | map, raise, `_NET_ACTIVE_WINDOW` | +| Wayland | commit again | null buffer and commit | `ErrNotSupported`: xdg-shell has no such request | +| macOS | `orderFront:` | `orderOut:` | activate the app, `makeKeyAndOrderFront:` | +| Windows | `SW_SHOWNA` | `SW_HIDE` | `SW_RESTORE` and `SetForegroundWindow` | +| GTK, Android, wasmbox | `ErrNotSupported` | `ErrNotSupported` | `ErrNotSupported` | + +Focus is the platform's to grant: under focus-stealing prevention, `Raise` +may only mark the window as wanting attention, and cannot tell. diff --git a/content/en/surfaces/snapshots.md b/content/en/surfaces/snapshots.md new file mode 100644 index 0000000..b592953 --- /dev/null +++ b/content/en/surfaces/snapshots.md @@ -0,0 +1,31 @@ +--- +title: "Snapshots: a widget as SVG or PNG" +linkTitle: "Snapshots" +weight: 60 +description: "go-widgets/svg wraps any RGBA render in a self-contained SVG, bit-exact, or writes it as a PNG." +tags: [surfaces, svg, png] +--- + +[`go-widgets/svg`](https://github.com/go-widgets/svg) turns an RGBA buffer into +a self-contained SVG: a `viewBox` pinned to the render's pixel size, and the +pixels as a base-64 PNG under `image-rendering: pixelated`. Every pixel shown +is the pixel the widget drew — no re-vectorisation, no font substitution — and +it stays sharp at any zoom. + +```go +// any RGBA producer +svg.Snapshot(f, surface, w, h, "my image") + +// a toolkit widget +btn := toolkit.NewButton("Click me", nil) +btn.SetBounds(toolkit.Rect{W: 200, H: 40}) +widget.Snapshot(f, btn, 200, 40, toolkit.DefaultLight(), "widget: button") +widget.PNG(p, btn, 200, 40, toolkit.DefaultLight()) +``` + +The label becomes the SVG's `<title>` and `aria-label`, escaped. The root +package depends on nothing; the `widget` subpackage is separate so a consumer +with its own pixel producer does not pull the toolkit into its module graph. + +`svg/cmd/gallery-render` renders the widget catalogue, light and dark: +[go-widgets.github.io/svg](https://go-widgets.github.io/svg/). diff --git a/content/en/surfaces/terminal.md b/content/en/surfaces/terminal.md new file mode 100644 index 0000000..88068cd --- /dev/null +++ b/content/en/surfaces/terminal.md @@ -0,0 +1,64 @@ +--- +title: "A terminal" +linkTitle: "Terminal" +weight: 40 +description: "go-widgets/tui renders widgets through the cell painter: a snapshot renderer, an interactive runner, and widgets sized for a character grid." +tags: [surfaces, tui, terminal] +--- + +[`go-widgets/tui`](https://github.com/go-widgets/tui) turns a widget tree into +a terminal frame through the [cell painter]({{< relref "/rendering/painter.md" >}}). +Standard library only — no `x/term`, no cgo. + +## One frame + +```go +_ = tui.RenderOnce(os.Stdout, widgets, nil) // nil theme: light +``` + +`RenderOnceSized(w, cols, rows, widgets, theme)` takes the size explicitly; +`RenderOnce` reads `COLUMNS` and `LINES` and falls back to 80×24. Reading the +environment rather than calling `TIOCGWINSZ` keeps the package free of +per-platform code. + +## An interactive program + +```go +app := tui.NewApp() +app.Root = buildScene() +app.Keys["q"] = func(a *tui.App) { a.Quit() } +app.Keys["Ctrl+C"] = func(a *tui.App) { a.Quit() } +os.Exit(app.Run()) +``` + +`Run` enters the alternate screen and raw mode, dispatches input to `Root` +(global key handlers first), follows `SIGWINCH`, and always restores the +terminal — on a panic too. A key handler calls `Consume` to keep an event from +`Root`; `InputTarget` sends every unconsumed event to one widget, which is how +a command palette swallows typing while it is open. `NewFocusRing` gives a +form Tab and Shift+Tab traversal. + +## Widgets sized for cells + +Most toolkit widgets are designed in pixels: their padding constants count +cells under the cell painter and inflate. So `tui` has its own **cell-native** +set — `TextEditor`, `Entry`, `Button`, `CheckButton`, `RadioButton`, `Scale`, +`ProgressBar`, `Sparkline`, `ListBox`, `TreeView`, `Table`, `MenuBar`, +`Notebook`, `HSplit`, `VSplit`, `Dialog`, `Dropdown`, `Spinner` and more — +where one glyph is one cell. Each is still a `toolkit.Widget`, so the same +instance also draws into pixels. + +`TextEditor` is a read-write code editor: gutter, undo and redo, search and +replace, selection, line moves, and syntax highlighting for Go (through +`go/scanner`), JavaScript and TypeScript, Python, Ruby, shell, C and C++, +Rust, JSON, YAML, HCL, TOML, LaTeX and Markdown, with no external dependency. + +```sh +go run ./cmd/tui-widgets | less -R # every cell-native widget, one per slot +go run ./cmd/tui-widget-explorer # the same, interactive +go run ./cmd/tui-explorer # a file browser +go run ./cmd/tui-editor --file=x.go # a modal editor +``` + +The two demo programs are tested end to end in a real pty: real key bytes in, +the rendered frame asserted. diff --git a/content/es/_index.md b/content/es/_index.md new file mode 100644 index 0000000..a2ca058 --- /dev/null +++ b/content/es/_index.md @@ -0,0 +1,76 @@ +--- +title: "go-widgets" +linkTitle: "Inicio" +type: docs +cascade: + type: docs +description: "Un conjunto de widgets en Go puro y todas las superficies sobre las que pinta: una ventana nativa en X11, Wayland, macOS y Windows, un canvas de navegador, un terminal, un APK de Android." +--- + +**Un único árbol de widgets, pintado por Go, en cada superficie a la que llega +Go.** Una ventana nativa en X11, Wayland, macOS y Windows; un `<canvas>` en una +pestaña del navegador; un terminal; un APK de Android. Sin cadena de herramientas +de C, sin webview, sin conjunto de widgets del sistema: todos los módulos se +compilan con `CGO_ENABLED=0`, y cada píxel lo dibuja el toolkit. + +```go +w, err := window.Open(window.Config{Title: "Hello"}) +if err != nil { + return err +} +defer w.Close() + +box := toolkit.NewVBox() +box.Append(toolkit.NewLabel("Hello from go-widgets")) +box.Append(toolkit.NewButton("Click me", func() { /* ... */ })) +return w.Run(box) // an X11 or Wayland window, an NSWindow, a Win32 window +``` + +Eso es +[`cmd/windowdemo`](https://github.com/go-widgets/window/blob/main/cmd/windowdemo/main.go), +abreviado. El mismo `box` dibujado en un navegador, un terminal o un PNG es el +mismo código con otra superficie debajo. + +## Por qué un solo toolkit lo dibuja todo {#why-one-toolkit-draws-everything} + +Un widget que recurre al botón propio de la plataforma es tan portable como la +plataforma menos capaz a la que se dirige, y tiene un aspecto distinto en cada +una. go-widgets va en sentido contrario: el `Draw` de un widget solo habla con un +[`painter.Painter`]({{< relref "/rendering/painter.md" >}}), un puñado de +primitivas (rellenar un rectángulo, trazar su contorno, fijar un píxel, dibujar +texto). Que esas llamadas se conviertan en píxeles RGBA o en celdas de terminal +es asunto del backend, no del widget. + +Lo que cuesta se dice sin rodeos: ningún aspecto nativo por defecto y ninguna +accesibilidad gratuita; el toolkit publica su propio +[árbol de accesibilidad]({{< relref "/rendering/toolkit.md#accessibility" >}}) +en cada plataforma. Lo que aporta es un único conjunto de widgets, probado una +sola vez, con el mismo comportamiento en todas partes, y un binario que se +compila de forma cruzada desde cualquier máquina hacia cualquiera de ellas. + +## Por dónde seguir {#where-to-go-next} + +| | | +|---|---| +| [Una primera aplicación]({{< relref "/getting-started.md" >}}) | desde `app-template`, o desde una ventana vacía | +| [Cómo llega un widget a la pantalla]({{< relref "/rendering/_index.md" >}}) | la costura del painter, el conjunto de widgets, las skins | +| [La costura del painter]({{< relref "/rendering/painter.md" >}}) | las primitivas con las que se dibuja cada widget | +| [El toolkit]({{< relref "/rendering/toolkit.md" >}}) | 160 tipos de widget, temas, layouts, texto, accesibilidad | +| [Skins]({{< relref "/rendering/skin.md" >}}) | las partes y los estados de un widget en un archivo de datos | +| [El estado vive en un view-model]({{< relref "/state/_index.md" >}}) | por qué los campos de los widgets no se asignan a mano | +| [MVVM]({{< relref "/state/mvvm.md" >}}) | observables, comandos, binders, deshacer | +| [La columna vertebral de datos]({{< relref "/state/data.md" >}}) | registros tipados, consultas, un almacén local o remoto | +| [Dónde pinta]({{< relref "/surfaces/_index.md" >}}) | cada backend y cómo se elige | +| [Una ventana nativa]({{< relref "/surfaces/native-window.md" >}}) | X11, Wayland, Cocoa, Win32, GTK4, Android, wasmbox | +| [Una aplicación y su bandeja]({{< relref "/surfaces/application.md" >}}) | ciclo de vida, apariencia, un icono en la barra de menús | +| [Una pestaña del navegador]({{< relref "/surfaces/browser.md" >}}) | un simple `<canvas>`, o una ventana de wasmdesk | +| [Un terminal]({{< relref "/surfaces/terminal.md" >}}) | widgets nativos de celdas y un ejecutor interactivo | +| [Android]({{< relref "/surfaces/android.md" >}}) | un APK real pintado por un proceso sin CGO | +| [Instantáneas]({{< relref "/surfaces/snapshots.md" >}}) | un widget como SVG o PNG | +| [Reglas que impone la CI]({{< relref "/guard-rails/_index.md" >}}) | `mvvmlint` y `bricolint` | +| [Módulos]({{< relref "/modules.md" >}}) | los diecinueve, con sus fuentes y referencias | +| [Estado]({{< relref "/status.md" >}}) | qué está verificado y dónde, y qué no está hecho | + +## Licencia {#licence} + +BSD-3-Clause. diff --git a/content/es/getting-started.md b/content/es/getting-started.md new file mode 100644 index 0000000..b7ab2c1 --- /dev/null +++ b/content/es/getting-started.md @@ -0,0 +1,81 @@ +--- +title: "Una primera aplicación" +weight: 5 +description: "Partir de app-template, que ya cumple MVVM y está en verde, o abrir una ventana vacía y añadirle widgets." +tags: [primeros pasos, app-template, ventana] +--- + +## Desde la plantilla {#from-the-template} + +[`go-widgets/app-template`](https://github.com/go-widgets/app-template) es un +repositorio plantilla: una aplicación de navegador pequeña pero real, con un +campo de búsqueda, un filtro por categoría, una lista de las filas que coinciden +y una línea de estado. Todo lo que necesita una aplicación más grande ya está +conectado: un view-model sin ningún widget dentro, una vista enlazada a él, un +host wasm, una barrera de cobertura del 100 % y la +[barrera MVVM]({{< relref "/guard-rails/mvvmlint.md" >}}). + +```sh +git clone https://github.com/go-widgets/app-template my-app +cd my-app +go mod edit -module github.com/you/my-app +./build.sh # dist/app.wasm, dist/wasm_exec.js, dist/index.html +``` + +Sirva `dist/` por HTTP: una página abierta desde `file://` no puede instanciar wasm. + +| Archivo | Qué contiene | +|---|---| +| `viewmodel.go` | todo el estado, como observables y comandos de [`mvvm`]({{< relref "/state/mvvm.md" >}}). Ningún widget. Se puede probar sin canvas. | +| `scene.go` | los widgets, cada uno enlazado al view-model mediante `mvvmtk`. Nunca asigna por sí mismo un campo de estado de un widget. | +| `main.go` | el único archivo con una etiqueta de compilación (`js && wasm`): reenvía la entrada al toolkit y copia el fotograma en un `<canvas>`. | + +Edite los dos primeros y haga que el estado siga pasando por los binders: el +trabajo `mvvm` de la CI rechaza una pull request que asigne directamente un +campo de un widget. + +## Desde una ventana vacía {#from-a-bare-window} + +```go +package main + +import ( + "github.com/go-widgets/toolkit" + "github.com/go-widgets/window" +) + +func main() { + w, err := window.Open(window.Config{Title: "Demo"}) + if err == window.ErrUnsupported { + return // no native back-end on this platform + } + if err != nil { + panic(err) + } + defer w.Close() + + box := toolkit.NewVBox() + box.Append(toolkit.NewLabel("Hello")) + box.Append(toolkit.NewButton("Click me", func() {})) + w.Run(box) // layout, draw, present, dispatch input, until closed +} +``` + +Un ancho y un alto dejados a cero piden al backend un tamaño por defecto +legible; en macOS es una fracción de la pantalla principal. `CGO_ENABLED=0 go build` +produce el binario para cada plataforma, desde cualquier máquina. + +## Qué módulo para qué {#which-module-for-what} + +| Quiere | Empiece por | +|---|---| +| una ventana de escritorio | [`window`]({{< relref "/surfaces/native-window.md" >}}) | +| una aplicación de escritorio con un icono en la bandeja y la apariencia del sistema | [`application`]({{< relref "/surfaces/application.md" >}}) | +| una página en un navegador | [`webcanvas`]({{< relref "/surfaces/browser.md" >}}), o la plantilla anterior | +| un programa de terminal | [`tui`]({{< relref "/surfaces/terminal.md" >}}) | +| una aplicación Android | [`android`]({{< relref "/surfaces/android.md" >}}) | +| una imagen de un widget para un README | [`svg`]({{< relref "/surfaces/snapshots.md" >}}) | + +Para ver el conjunto de widgets antes de escribir nada, abra la +[galería](https://go-widgets.github.io/gallery/): todas las familias de widgets, +en vivo, en un canvas de navegador. diff --git a/content/es/guard-rails/_index.md b/content/es/guard-rails/_index.md new file mode 100644 index 0000000..876c77c --- /dev/null +++ b/content/es/guard-rails/_index.md @@ -0,0 +1,25 @@ +--- +title: "Reglas que impone la CI" +linkTitle: "Reglas de CI" +weight: 40 +description: "Dos comprobaciones go/analysis que una aplicación ejecuta como comprobaciones de estado obligatorias: el estado pasa por MVVM, y nada de interfaz dibujada a mano." +tags: [ci, lint] +--- + +Dos convenciones mantienen honesta la interfaz de una aplicación, y ambas se +irían erosionando, un atajo cómodo tras otro, si solo estuvieran escritas. Por +eso cada una es un analizador +[`go/analysis`](https://pkg.go.dev/golang.org/x/tools/go/analysis), +ejecutado mediante `go vet`, con un workflow reutilizable que una aplicación +marca como comprobación **obligatoria**. + +| | Rechaza | +|---|---| +| [`mvvmlint`]({{< relref "/guard-rails/mvvmlint.md" >}}) | asignar a mano un campo de estado de un widget, y un paquete con widgets pero sin view-model | +| [`bricolint`]({{< relref "/guard-rails/bricolint.md" >}}) | primitivas del painter en el código de la aplicación, y un widget construido de nuevo en cada fotograma | + +Ambas son conservadoras por construcción: una regla solo salta cuando el **tipo +estático** del receptor se resuelve, mediante `go/types`, a un tipo del toolkit o +del painter. Un campo o un método con el mismo nombre en un tipo no relacionado +nunca se señala. Las excepciones previstas son directivas explícitas en el +código, de modo que cada una queda escrita allí donde se hace. diff --git a/content/es/guard-rails/bricolint.md b/content/es/guard-rails/bricolint.md new file mode 100644 index 0000000..07c1d78 --- /dev/null +++ b/content/es/guard-rails/bricolint.md @@ -0,0 +1,55 @@ +--- +title: "bricolint: nada de interfaz dibujada a mano" +linkTitle: "bricolint" +weight: 20 +description: "Rechaza una pull request que pinta elementos de interfaz con primitivas del painter en el código de la aplicación, o que reconstruye un widget en cada fotograma." +tags: [ci, lint, painter] +--- + +El *bricolage* —dibujo improvisado, hecho a mano— es la manera en que una +interfaz pierde sin hacer ruido su respuesta visual al pulsar, al pasar el +puntero y al recibir el foco, sus temas, su escalado HiDPI y su accesibilidad. +[`go-widgets/bricolint`](https://github.com/go-widgets/bricolint) +impide que una aplicación que ya se ha pasado al toolkit vuelva atrás. + +```go +p.FillRect(bar, theme.Bg) // flagged: hand-drawn chrome +toolkit.NewBackdrop(theme.Bg).Draw(p) // a toolkit widget + +func (v *view) Draw(p painter.Painter) { + b := toolkit.NewButton("ok") // flagged: a new widget every frame + b.Draw(p) +} +``` + +1. **Una primitiva del painter en el código de la aplicación**: un método de + dibujo (`FillRect`, `StrokeRect`, `FillPath`, `DrawImage`, `Text`, + `PutPixel`, …) invocado sobre un receptor cuyo tipo estático es un tipo de + painter. Consultar o recortar la superficie (`Size`, `PushClip`, + `PushTranslate`) no es dibujar y no se señala. `-primitives=` sustituye la lista. +2. **Un widget desechado en cada fotograma**: una llamada `toolkit.New…` dentro + de un método llamado `Draw`, `Paint` o `Render`. Un widget reconstruido en + cada pintado no conserva ningún estado de interacción. Constrúyalo una vez, + guárdelo en un campo y contrólelo mediante un enlace. `-checkthrow=false` + desactiva esta regla. + +Hay código que es una auténtica hoja —un framebuffer de juego, un backend de +painter, las entrañas del propio toolkit— y lo declara explícitamente: + +```go +p.FillRect(bg, c) //bricolint:allow engine SVG raster blit — a genuine leaf +``` + +```go +//bricolint:allowfile painter back-end — this file IS the leaf +package pdfsurface +``` + +**La razón es obligatoria**: una directiva sin ella se ignora, y el hallazgo +sigue saltando hasta que alguien deja escrita la justificación. + +```yaml +jobs: + bricolint: + uses: go-widgets/bricolint/.github/workflows/bricolint.yml@main +``` diff --git a/content/es/guard-rails/mvvmlint.md b/content/es/guard-rails/mvvmlint.md new file mode 100644 index 0000000..b0c5a2d --- /dev/null +++ b/content/es/guard-rails/mvvmlint.md @@ -0,0 +1,44 @@ +--- +title: "mvvmlint: el estado pasa por el view-model" +linkTitle: "mvvmlint" +weight: 10 +description: "Rechaza una pull request que asigna directamente un campo de estado de un widget del toolkit, o que tiene widgets y ningún view-model." +tags: [ci, lint, mvvm] +--- + +[`go-widgets/mvvmlint`](https://github.com/go-widgets/mvvmlint) solo actúa en +los paquetes que importan el toolkit. + +1. **Un campo de estado de un widget asignado a mano.** `entry.Text = "x"`, + donde el tipo estático de `entry` es un widget del toolkit y `Text` es un + campo de estado, se señala. `&entry.Text` —lo que reciben los + [binders]({{< relref "/state/mvvm.md" >}})— no es una asignación y nunca se + señala; tampoco un literal compuesto (`toolkit.Entry{Text: "x"}`), ni la + composición de un callback como `OnChange`. +2. **Widgets y ningún view-model.** Un paquete que importa el toolkit pero no + `go-widgets/mvvm` recibe un diagnóstico. `-requirevm=false` lo desactiva para + una biblioteca de widgets que, con razón, no tiene view-model. + +Los campos de estado son los de los widgets del toolkit (`Text`, `Items`, +`Rows`, `Root`, `Selected`, `Value`, `Checked`, …); `-statefields=` sustituye la +lista. Un archivo llamado `*_binding.go`, o que contenga `//mvvmlint:allow`, +queda exento de la primera regla. + +```sh +go install github.com/go-widgets/mvvmlint/cmd/mvvmlint@latest +go vet -vettool="$(go env GOPATH)/bin/mvvmlint" ./... +``` + +```yaml +# .github/workflows/mvvm.yml +name: mvvm +on: + pull_request: + push: + branches: [main] +jobs: + mvvm: + uses: go-widgets/mvvmlint/.github/workflows/mvvmlint.yml@main +``` + +[`app-template`]({{< relref "/getting-started.md" >}}) incluye este trabajo de serie. diff --git a/content/es/modules.md b/content/es/modules.md new file mode 100644 index 0000000..2acd140 --- /dev/null +++ b/content/es/modules.md @@ -0,0 +1,48 @@ +--- +title: "Módulos" +weight: 50 +description: "Los diecinueve módulos de go-widgets, para qué sirve cada uno y dónde están su código fuente y su referencia de API." +tags: [módulos] +--- + +La referencia de API la genera a partir del código fuente +[pkg.go.dev](https://pkg.go.dev/), que sigue las etiquetas publicadas; estas +páginas explican, y no la copian. La etiqueta es la más reciente en el momento +de escribir; la autoridad es la lista de etiquetas del propio repositorio. + +| Módulo | Etiqueta | Qué es | Referencia | +|---|---|---|---| +| [`painter`](https://github.com/go-widgets/painter) | v0.15.0 | [la costura de dibujo]({{< relref "/rendering/painter.md" >}}): painters de píxeles y de celdas | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/painter) | +| [`toolkit`](https://github.com/go-widgets/toolkit) | v0.328.0 | [el conjunto de widgets]({{< relref "/rendering/toolkit.md" >}}), temas, layouts, texto | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/toolkit) | +| [`skin`](https://github.com/go-widgets/skin) | v0.2.0 | [widgets descritos como datos]({{< relref "/rendering/skin.md" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/skin) | +| [`isoicons`](https://github.com/go-widgets/isoicons) | v0.3.0 | paquetes de iconos isométricos (cloud-native, AWS) para los diagramas isométricos del toolkit | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/isoicons) | +| [`mvvm`](https://github.com/go-widgets/mvvm) | v0.13.0 | [observables, comandos, binders, deshacer]({{< relref "/state/mvvm.md" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/mvvm) | +| [`mvvmtk`](https://github.com/go-widgets/mvvmtk) | v0.14.1 | [binders de una sola llamada para los widgets del toolkit]({{< relref "/state/mvvm.md#mvvmtk-one-call-per-widget" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/mvvmtk) | +| [`data`](https://github.com/go-widgets/data) | v0.3.0 | [registros tipados, consultas, un almacén local o remoto]({{< relref "/state/data.md" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/data) | +| [`window`](https://github.com/go-widgets/window) | v0.86.1 | [una ventana nativa]({{< relref "/surfaces/native-window.md" >}}) sobre siete backends | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/window) | +| [`application`](https://github.com/go-widgets/application) | v0.7.0 | [el ciclo de vida de la aplicación]({{< relref "/surfaces/application.md" >}}) por encima de una ventana | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/application) | +| [`tray`](https://github.com/go-widgets/tray) | v0.14.0 | [un icono de bandeja y su menú]({{< relref "/surfaces/application.md#the-tray" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/tray) | +| [`webcanvas`](https://github.com/go-widgets/webcanvas) | v0.2.0 | [una escena en un `<canvas>` de navegador]({{< relref "/surfaces/browser.md" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/webcanvas) | +| [`tui`](https://github.com/go-widgets/tui) | v0.61.0 | [renderizado en terminal y widgets nativos de celdas]({{< relref "/surfaces/terminal.md" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/tui) | +| [`android`](https://github.com/go-widgets/android) | v0.15.0 | [un APK pintado por un proceso sin CGO]({{< relref "/surfaces/android.md" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/android) | +| [`svg`](https://github.com/go-widgets/svg) | v0.6.0 | [un renderizado como SVG o PNG]({{< relref "/surfaces/snapshots.md" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/svg) | +| [`mvvmlint`](https://github.com/go-widgets/mvvmlint) | v0.4.0 | [la comprobación MVVM]({{< relref "/guard-rails/mvvmlint.md" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/mvvmlint) | +| [`bricolint`](https://github.com/go-widgets/bricolint) | v0.4.0 | [la comprobación de que no haya interfaz dibujada a mano]({{< relref "/guard-rails/bricolint.md" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/bricolint) | +| [`app-template`](https://github.com/go-widgets/app-template) | v0.4.0 | [el punto de partida de una aplicación]({{< relref "/getting-started.md" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/app-template) | +| [`gallery`](https://github.com/go-widgets/gallery) | v0.6.0 | la [demo en vivo](https://go-widgets.github.io/gallery/) de todas las familias de widgets | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/gallery) | +| [`desktop`](https://github.com/go-widgets/desktop) | v0.19.0 | un shell de escritorio que combina go-freedesktop y go-widgets, nativo y en wasmdesk | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/desktop) | + +## Lo que se exige a cada módulo {#what-every-module-is-held-to} + +- `CGO_ENABLED=0`: nada de cgo, ni de invocar una herramienta de línea de + comandos en lugar de una biblioteca. +- Compilación cruzada para amd64, arm64, riscv64, loong64, ppc64le y s390x + —esta última big-endian, lo que mantiene honesta cada codificación binaria— y, + cuando procede, `js/wasm`. Doce de los diecinueve cubren hoy las seis, dos son + aplicaciones de navegador y los otros cinco figuran en [Estado]({{< relref "/status.md#what-ci-does-not-cover-yet" >}}). +- Una barrera de cobertura de sentencias en la CI: 100 % en dieciocho de los + diecinueve. Cuando un archivo no puede ejecutarse en una prueba (un bucle de + navegador, un bucle de ejecución nativo), el módulo indica qué archivo queda + fuera y por qué. Consulte [Estado]({{< relref "/status.md" >}}) para las + excepciones. +- BSD-3-Clause. diff --git a/content/es/rendering/_index.md b/content/es/rendering/_index.md new file mode 100644 index 0000000..73ee966 --- /dev/null +++ b/content/es/rendering/_index.md @@ -0,0 +1,30 @@ +--- +title: "Cómo llega un widget a la pantalla" +linkTitle: "Renderizado" +weight: 10 +description: "Un widget dibuja a través de un painter; el painter decide si eso se convierte en píxeles o en celdas de terminal; un backend presenta el resultado." +tags: [renderizado, painter, toolkit] +--- + +Tres capas, y cada una solo conoce la que tiene debajo: + +```text +toolkit widget tree Button, Table, Agenda, VBox ... (or a skin.Object) + │ Draw(p painter.Painter, theme *Theme) + ▼ +painter.Painter FillRect, StrokeRect, Text, PutPixel, ... + │ + ├─ PixelPainter → an RGBA []byte → window, browser canvas, PNG, Android + └─ CellPainter → a grid of cells → a terminal, as 24-bit ANSI +``` + +Un widget nunca sabe qué painter ha recibido. Un backend nunca sabe qué +widgets han dibujado. Esa es toda la razón por la que un mismo árbol de widgets +puede mostrarse en una ventana nativa, un navegador y un terminal sin una sola +línea de código condicional. + +| | | +|---|---| +| [La costura del painter]({{< relref "/rendering/painter.md" >}}) | las primitivas, las capacidades opcionales, los dos painters | +| [El toolkit]({{< relref "/rendering/toolkit.md" >}}) | el conjunto de widgets, temas, layouts, texto y accesibilidad | +| [Skins]({{< relref "/rendering/skin.md" >}}) | describir las partes y los estados de un widget como datos en lugar de en Go | diff --git a/content/es/rendering/painter.md b/content/es/rendering/painter.md new file mode 100644 index 0000000..34522b0 --- /dev/null +++ b/content/es/rendering/painter.md @@ -0,0 +1,75 @@ +--- +title: "La costura del painter" +linkTitle: "Painter" +weight: 10 +description: "Las primitivas con las que se dibuja cada widget, las capacidades opcionales que un painter puede añadir, y los painters de píxeles y de celdas." +tags: [renderizado, painter] +--- + +[`go-widgets/painter`](https://github.com/go-widgets/painter) define la única +interfaz a través de la cual dibuja cada widget: + +```go +type Painter interface { + FillRect(r Rect, c RGBA) + StrokeRect(r Rect, c RGBA, lineW int) + FillRoundRect(r Rect, radius int, c RGBA) + StrokeRoundRect(r Rect, radius int, c RGBA, lineW int) + PutPixel(x, y int, c RGBA) + Text(x, y int, s string, ink RGBA) + Size() (w, h int) +} +``` + +El conjunto es pequeño a propósito: cada backend debe implementarlo entero, y +una cuadrícula de terminal no puede redondear una esquina ni variar el grosor +de un trazo. Por eso el contrato dice qué ocurre cuando no puede: el grosor de +`StrokeRect` es una indicación que una cuadrícula de celdas ignora, y +`FillRoundRect` recurre allí a un relleno cuadrado. + +## Capacidades opcionales {#optional-capabilities} + +Lo que no todas las superficies pueden hacer no está en la interfaz base. Un +widget que lo necesita hace una aserción de tipo, y dibuja algo razonable +cuando la respuesta es no: + +| Interfaz | Métodos | Se usa para | +|---|---|---| +| `Clipper` | `PushClip`, `PopClip` | mantener un hijo desplazado dentro de los límites de su padre | +| `Translator` | `PushTranslate`, `PopTranslate` | dibujar un hijo en sus propias coordenadas | +| `PathPainter` | `FillPath`, `StrokePath` | curvas, gráficos, formas con antialiasing | +| `ImagePainter` | `DrawImage` | iconos, miniaturas, imágenes | +| `MaskPainter` | `DrawMask` | glifos y otras máscaras de cobertura en un solo color | +| `FacePainter` | `TextFace` | texto en una tipografía y un tamaño concretos | + +```go +if c, ok := p.(painter.Clipper); ok { + c.PushClip(bounds) + defer c.PopClip() +} +``` + +## Los dos painters {#the-two-painters} + +| | Escribe en | Lo presenta | +|---|---|---| +| `NewPixelPainter(buf, w, h)` | un `[]byte` RGBA propiedad del llamante (`NewPixelPainterBGRA` para superficies BGRA) | una ventana nativa, un canvas de navegador, una superficie Android, un PNG o un SVG | +| `NewCellPainter(w, h)` | una cuadrícula de celdas (runa, primer plano, fondo) con un serializador ANSI de 24 bits | un terminal, a través de [`tui`]({{< relref "/surfaces/terminal.md" >}}) | + +El búfer sigue perteneciendo al llamante. Un `PixelPainter` solo traduce +primitivas en escrituras, de modo que el mismo `[]byte` puede entregarse a un +backend de ventana, a un `<canvas>` o a un codificador de imágenes sin ninguna +copia intermedia. + +## Probarlo {#trying-it} + +Las demos del propio repositorio dibujan los mismos tres widgets a través de +ambos painters: + +```sh +go run ./cmd/wui-demo --out demo.png # pixels, written as a PNG +go run ./cmd/tui-demo --theme dark # cells, written as ANSI to stdout +``` + +y [go-widgets.github.io/painter](https://go-widgets.github.io/painter/) muestra +el painter de píxeles en un navegador. diff --git a/content/es/rendering/skin.md b/content/es/rendering/skin.md new file mode 100644 index 0000000..19a66d2 --- /dev/null +++ b/content/es/rendering/skin.md @@ -0,0 +1,86 @@ +--- +title: "Skins: las partes y los estados de un widget como datos" +linkTitle: "Skins" +weight: 30 +description: "go-widgets/skin describe las partes de un widget, su aspecto en cada estado y las transiciones provocadas por señales en un archivo .skin.json, renderizado por un único motor." +tags: [renderizado, skin, tema] +--- + +Los widgets del toolkit están escritos en Go: las partes de un `Button`, sus +aspectos al pasar el puntero, al pulsarse y deshabilitado, y su respuesta al +clic viven todos en `button.go`. Es rápido y exacto, y cada cambio visual es un +cambio de código. + +[`go-widgets/skin`](https://github.com/go-widgets/skin) traslada esa descripción +a datos: la idea del Edje de Enlightenment. Un documento `.skin.json` nombra las +**partes** de un widget, su aspecto en cada **estado** y los **programas** que +las hacen pasar de un estado a otro cuando llega una señal. Cambiar la skin, o +construir un control nuevo, ya no requiere recompilar: el programa carga otros +bytes. + +```go +theme, err := skin.Load(jsonBytes) // parse and validate every collection +if err != nil { + return err +} +obj, _ := theme.New("button") +obj.Bind(skin.NewMVVMContext().Set("$.label", labelObs)) +obj.SetBounds(toolkit.Rect{W: 96, H: 28}) + +// each frame +obj.OnEvent(ev) // pointer events become signals, signals states +busy := obj.Tick(time.Now()) // advance the transitions +obj.Draw(p, theme.Palette()) +``` + +Un `skin.Object` es un `toolkit.Widget`, así que se coloca en un `VBox` junto a +widgets escritos a mano. Los layouts colocan widgets; una skin organiza lo que +hay dentro de uno. + +## El formato {#the-format} + +```jsonc +{ + "collections": { + "button": { + "min": { "w": 96, "h": 28 }, + "parts": [ + { "name": "bg", "type": "rect", + "states": { + "default": { "color": "@surface", "border": "@border", "radius": 6 }, + "hover": { "color": "@surface_alt", "border": "@border", "radius": 6 }, + "pressed": { "color": "@accent", "border": "@border", "radius": 6 } } }, + { "name": "label", "type": "text", "text_from": "$.label", "align": [0.5, 0.5], + "states": { "default": { "ink": "@on_surface" } } } + ], + "programs": [ + { "on": "mouse,in", "target": ["bg", "label"], "to": "hover", "in": 0.12, "ease": "ease_out_cubic" }, + { "on": "mouse,click", "emit": "clicked" } + ] + } + } +} +``` + +- Una **parte** es un `rect`, un `text` o una `image`, situada mediante dos + esquinas relativas (`rel1`, `rel2`) respecto al objeto o a una parte + anterior, con desplazamientos en píxeles y relativos a la fuente + (`offset_em`), de modo que una parte de texto sigue a la fuente. +- Un **estado** fija colores, borde, radio y visibilidad, y puede mover o + redimensionar la parte. +- Un **programa** reacciona a una señal, lleva partes a un estado durante un + tiempo con una curva de easing con nombre, y puede emitir (`emit`) una señal + hacia fuera: hacia un [`mvvm.Command`]({{< relref "/state/mvvm.md" >}}), por + ejemplo. +- Los colores son `@tokens` resueltos contra el `Theme` del toolkit (incluida + cada entrada `@define-color` de GTK como `@extra:<name>`), hexadecimales o + arrays RGBA. + +El JSON lo decodifica la biblioteca estándar: ninguna dependencia de terceros. + +## Contrastado con los widgets escritos a mano {#proven-against-the-hand-written-widgets} + +El botón, la casilla de verificación, el interruptor, el chip y la tarjeta del +toolkit están reescritos cada uno como una colección de skin, y las pruebas +comprueban que ambos se renderizan **idénticos píxel a píxel** en todos los +estados. Un benchmark mide un `Draw` con skin frente al escrito a mano. diff --git a/content/es/rendering/toolkit.md b/content/es/rendering/toolkit.md new file mode 100644 index 0000000..2ea2a5c --- /dev/null +++ b/content/es/rendering/toolkit.md @@ -0,0 +1,77 @@ +--- +title: "El toolkit" +linkTitle: "Toolkit" +weight: 20 +description: "160 tipos de widget dibujados a través de la costura del painter: temas, layouts, texto, entrada, accesibilidad." +tags: [renderizado, toolkit, widgets, tema, accesibilidad] +--- + +[`go-widgets/toolkit`](https://github.com/go-widgets/toolkit) es el conjunto de +widgets. Todos los widgets implementan la misma interfaz —`Bounds`, `SetBounds`, +`Draw(p painter.Painter, theme *Theme)`, `HitTest`, `OnEvent`—, de modo que un +host trata igual un botón, una tabla y un calendario de eventos. Solo la +biblioteca estándar, `CGO_ENABLED=0`, 100 % de cobertura de sentencias. + +En la v0.328.0 tiene **160 tipos de widget exportados** en unas **69 000 líneas** +de código de widgets. Esas dos cifras se midieron en esa etiqueta, no se +arrastraron de antes: el README del toolkit da los comandos, y conviene volver +a ejecutarlos en lugar de repetir las cifras. + +## Las familias {#the-families} + +| Familia | Widgets | +|---|---| +| Acción | `Button`, `ToggleButton`, `CheckButton`, `RadioButton` + `RadioGroup`, `Switch`, `SplitButton`, `IconButton`, `CycleButton`, `Chip`, `SegmentedBar` | +| Entrada | `Entry`, `TextView` (selección, vista previa de IME, tramos de sintaxis, números de línea), `SpinButton`, `Scale`, `RangeSlider`, `SearchEntry`, `TagField`, `ComboBox`, `FormField` con `Validate`/`Rule` | +| Selección | `ListBox`, `TreeView`, `DropDown` | +| Contenedores | `Container` con un `Layout` intercambiable (`FitLayout`, `BoxLayout`, `BorderLayout`, `CardLayout`, `FlowLayout`); `HBox`, `VBox`, `Grid`, `Frame`, `Dock`, `Border`, `Stack`, `Overlay`, `Paned`, `Expander`, `Accordion`; un constructor declarativo `Node` | +| Navegación | `Menu`, `MenuBar` (con `Alt`+letra), `ContextMenu`, `Popover`, `CommandPalette`, `Dialog`, `MessageDialog`, `Wizard`, `Notebook`, `ViewSwitcher`, `Carousel` | +| Respuesta | `ProgressBar`, `ProgressCircle`, `LevelBar`, `Spinner`, `Tooltip`, `Notification`, `Toast`, `Banner`, `Alert`, `Badge`, `Skeleton` | +| Datos | `Table` (edición de celdas, columnas fijas, filas de grupo, agregados, expansores de fila), `TreeTable`, `PropertyGrid`, `Kanban`, `Gantt`, `Agenda` (semana, mes, trimestre, año; varios calendarios) | +| Gráficos | `LineChart`, `BarChart`, `PieChart`, `AreaChart`, `ScatterChart`, `RadarChart`, `Gauge`, `Sparkline` | +| Compuestos | `FileChooser`, `ColorChooser`, `ColorPicker`, `FontChooser`, `Calendar`, `DatePicker`, `DateRangePicker`, `TimePicker`, `MarkdownView`, `MarkdownEditor`, `TerminalView` | +| Shell | `Window` con decoraciones del lado del cliente, `HeaderBar`, `Toolbar`, `Statusbar`, `StatusIcon`, `StatusArea`, `Wallpaper`, `Thumbnail` | + +Todos ellos, en vivo: la [galería](https://go-widgets.github.io/gallery/). + +## Temas {#themes} + +Un único valor `Theme` se propaga en cascada por todo el árbol: cambie un color +en la raíz y todos los widgets se repintan con él. `DefaultLight()` y +`DefaultDark()` vienen incorporados, y `LoadGTKTheme(css)` lee el bloque +`@define-color` de cualquier tema de GTK 3 o libadwaita —Adwaita, WhiteSur, +Solarized— y lo convierte en un `Theme`. + +## Texto {#text} + +La fuente compilada por defecto es una fuente de mapa de bits de 5×7, para que +las pruebas de píxeles conserven su geometría. Una sola llamada pasa toda la +interfaz a texto con antialiasing y conformado: + +```go +toolkit.UseOpenTypeText() // Atkinson Hyperlegible, bundled; js/wasm-safe +``` + +La tipografía procede de [go-opentype/fonts](https://github.com/go-opentype/fonts) +y la rasteriza [go-opentype](https://github.com/go-opentype/opentype), en Go, +sin biblioteca de fuentes en C ni búsqueda de fuentes del sistema. +`NewTrueTypeFont(ttf, px)` carga otra tipografía; `NewFallbackFont` encadena +varias, por ejemplo para añadir una tipografía CJK o árabe; el campo `Font` +propio de un widget sustituye la global solo para ese widget. + +## Entrada {#input} + +Ratón, teclado, composición IME, táctil (`EventTouchStart`/`Move`/`End`, con +`GestureRecognizer` para toques, pulsaciones largas y deslizamientos), +arrastrar y soltar (`DragSource`, `DropTarget`). La selección de varias filas en +`ListBox`, `Table` y `TreeView` funciona con el ratón y con el teclado, y +respeta ⌘ además de Ctrl, porque en macOS Ctrl-clic es el clic secundario. + +## Accesibilidad {#accessibility} + +Un toolkit que pinta sus propios píxeles no da a un lector de pantalla nada que +leer a menos que diga lo que ha dibujado. Los widgets implementan `Accessible` +—un rol, un nombre, un valor— y `CollectA11y` los reúne para que el host los +publique: AT-SPI en Linux a través de +[`window`]({{< relref "/surfaces/native-window.md" >}}), y una jerarquía de +vistas virtual en [Android]({{< relref "/surfaces/android.md#accessibility" >}}). diff --git a/content/es/state/_index.md b/content/es/state/_index.md new file mode 100644 index 0000000..b0fae58 --- /dev/null +++ b/content/es/state/_index.md @@ -0,0 +1,24 @@ +--- +title: "El estado vive en un view-model" +linkTitle: "Gestión del estado" +weight: 20 +description: "Por qué el estado de la aplicación se guarda en observables enlazados a los widgets, en lugar de escribirse en los campos de los widgets." +tags: [gestión del estado, mvvm] +--- + +Los campos de un widget —el texto de una entrada, los elementos de una lista, +las filas de una tabla— son su **vista** del estado, no el estado. Una +aplicación que los asigna desde dondequiera que se encuentre dispersa su estado +entre los puntos de llamada, y dos de esos puntos acaban por no ponerse de +acuerdo sobre lo que hay en pantalla. + +Por eso el estado vive en un **view-model**: valores Go corrientes envueltos en +observables, sin ningún widget a la vista, que se pueden probar sin ventana. La +vista enlaza cada widget al view-model una sola vez, y a partir de ahí es el +enlace —no la aplicación— el que los mantiene sincronizados, en ambos sentidos. + +| | | +|---|---| +| [MVVM]({{< relref "/state/mvvm.md" >}}) | `Observable`, `Command`, `ObservableList`; binders para el toolkit y los widgets de terminal; deshacer y rehacer | +| [La columna vertebral de datos]({{< relref "/state/data.md" >}}) | registros tipados con validación, consultas y un almacén que ejecuta la misma cadena de procesamiento en local o por gRPC | +| [`mvvmlint`]({{< relref "/guard-rails/mvvmlint.md" >}}) | la comprobación de CI que rechaza una pull request que asigna a mano un campo de estado de un widget | diff --git a/content/es/state/data.md b/content/es/state/data.md new file mode 100644 index 0000000..c861949 --- /dev/null +++ b/content/es/state/data.md @@ -0,0 +1,46 @@ +--- +title: "La columna vertebral de datos" +linkTitle: "Datos" +weight: 20 +description: "go-widgets/data: registros tipados con validación, un motor de consultas puro y un almacén que ejecuta la misma cadena de procesamiento dentro del proceso o por gRPC, en nativo y en wasm." +tags: [gestión del estado, datos, grpc] +--- + +[`go-widgets/data`](https://github.com/go-widgets/data) no tiene interfaz: +importa la biblioteca estándar y [`mvvm`]({{< relref "/state/mvvm.md" >}}), +y ningún widget. + +| Pieza | Qué es | +|---|---| +| `Value`, `Kind` | un escalar tipado comparable: string, int, float, bool | +| `Record`, `Schema`, `Field`, `Rule` | una fila tipada y su validación | +| `Query` → `Apply` → `View` | el motor de consultas: filtrar, ordenar, agrupar, paginar, agregar | +| `Proxy` | la costura del backend: `List`, `Query`, `Mutate` | +| `MemoryProxy` | el backend dentro del proceso | +| `grpcproxy.Server`, `grpcproxy.Client` | el mismo contrato por gRPC, transportado sobre un WebSocket | +| `Store[R]` | una colección tipada, publicada como `mvvm.ObservableList` | + +## La misma respuesta, en local o en remoto {#the-same-answer-local-or-remote} + +`Store` habla con un `Proxy` y nunca sabe con cuál. `Apply` es una función pura +de los registros y la consulta, de modo que el cliente y el servidor ejecutan el +mismo código sobre las mismas filas, y un `MemoryProxy` y un `grpcproxy.Client` +devuelven vistas idénticas byte a byte. La prueba de conformidad comprueba +exactamente eso: una batería de consultas de ordenación, filtrado, agrupación, +paginación y agregación a través de ambos proxies y del motor, cada resultado +canonicalizado, y los tres comparados byte a byte. + +El transporte WebSocket compila para `js/wasm`, así que una aplicación de +navegador llega al mismo servicio a través del mismo cliente. + +```go +store := data.NewStore(mem, codec) // or a grpcproxy.Client: nothing else changes +store.SetQuery(data.Query{Sorts: []data.Sort{{Field: "salary", Desc: true}}, Limit: 20}) +store.Load(ctx) +items := store.Items() // bind this to a view +``` + +`tkbind.BindTable` coloca un almacén detrás de un `Table` del toolkit: un clic +en la cabecera ordena, una agrupación agrupa, y la edición validada de una celda +se convierte en una mutación a través del proxy: el mismo viaje de ida y vuelta, +en local o en remoto. diff --git a/content/es/state/mvvm.md b/content/es/state/mvvm.md new file mode 100644 index 0000000..a28f567 --- /dev/null +++ b/content/es/state/mvvm.md @@ -0,0 +1,98 @@ +--- +title: "MVVM: observables, comandos, binders" +linkTitle: "MVVM" +weight: 10 +description: "go-widgets/mvvm, sus binders para el toolkit y el terminal, las funciones de una sola llamada de mvvmtk y una pila de deshacer." +tags: [gestión del estado, mvvm, mvvmtk, deshacer] +--- + +[`go-widgets/mvvm`](https://github.com/go-widgets/mvvm) usa solo genéricos, sin +reflexión. Su paquete central no importa ningún paquete de widgets, de modo que +un mismo view-model controla tanto el [toolkit]({{< relref "/rendering/toolkit.md" >}}) +de píxeles como los [widgets de terminal]({{< relref "/surfaces/terminal.md" >}}). + +| Primitiva | Papel | +|---|---| +| `Observable[T]` | una propiedad: `Get`, `Set`, `Subscribe`. Asignar un valor igual no hace nada. | +| `Command` | una acción, con `CanExecute` y `RaiseCanExecuteChanged` | +| `ObservableList[T]` | una colección que notifica cada inserción, eliminación, sustitución, desplazamiento y reinicio | + +## Un formulario {#a-form} + +```go +type FormVM struct { + Name *mvvm.Observable[string] + Names *mvvm.ObservableList[string] + Save *mvvm.Command +} + +vm := &FormVM{Name: mvvm.NewObservable(""), Names: mvvm.NewObservableList[string]()} +vm.Save = mvvm.NewCommand( + func() { vm.Names.Append(vm.Name.Get()); vm.Name.Set("") }, + func() bool { return vm.Name.Get() != "" }, // CanExecute +) +mvvm.BindCanExecute(vm.Save, vm.Name) // Save greys out while the name is empty +``` + +## Enlace {#binding} + +Los binders genéricos llegan a un widget mediante **un puntero a su campo de +valor y un puntero a su ranura de callback**: `BindField` (bidireccional), +`OneWay`, `BindCommand`, `BindList`. Un widget dispara su callback desde su +manejador de eventos y deja en silencio una escritura directa del campo, y +`Observable.Set` ignora un valor igual: así, un enlace bidireccional puede +hacer eco sin entrar en recursión. + +El toolkit está pasando sus widgets a exponer su estado **como** un observable +(`Label.Text()` devuelve un `*mvvm.Observable[string]`). Entonces no hay ningún +campo que inicializar ni ningún callback que componer, solo dos propiedades que +deben coincidir: `BindTwoWay(src, dst, invalidate)`. + +Los widgets cuyo callback recibe varios argumentos tienen un pequeño adaptador +con nombre en un paquete por backend, los únicos paquetes que importan un +backend: + +| Paquete | Adaptadores | +|---|---| +| `mvvm/tkbind` | `BindRange` (el `RangeSlider` de dos tiradores), `BindContainer` y `BindCardActive` (los hijos de un contenedor y un card layout a partir de una lista o de un índice), `BindStore` y `BindTable` (un `Table` sobre un [almacén de datos]({{< relref "/state/data.md" >}}): ordenación, agrupación y ediciones en línea validadas) | +| `mvvm/tuibind` | `BindDropdown`, `BindTableSelection` | + +## mvvmtk: una llamada por widget {#mvvmtk-one-call-per-widget} + +[`go-widgets/mvvmtk`](https://github.com/go-widgets/mvvmtk) rellena los nombres +reales del campo y del callback de cada widget del toolkit, de modo que la vista +nunca los nombra: + +```go +unbind := mvvmtk.BindText(search, vm.Query, win.Invalidate) // search.Text ⇄ vm.Query +defer unbind() +``` + +| Función | Sentido | +|---|---| +| `BindText` (`SearchEntry`), `BindEntryText`, `BindChecked`, `BindSelectedIndex`, `BindSpin`, `BindListSelection`, `BindViewSwitcher` | bidireccional | +| `BindLabel`, `BindProgress` | view-model → widget | +| `BindListItems`, `BindDropDownOptions`, `BindViews`, `BindTree` | lista → widget | +| `BindCommand` | el clic de un botón, su estado `Disabled` y su atenuado siguen al comando | + +Cada función devuelve un `unbind` que desconecta el enlace y restaura el +callback que había antes. Un botón enlazado a un comando que no puede ejecutarse +no acepta ningún clic, ni Intro ni Espacio, ni el foco del teclado. + +## Deshacer y rehacer {#undo-and-redo} + +`mvvm/undo` es una pila de comandos construida únicamente sobre las primitivas +centrales, así que funciona bajo cualquier vista: + +```go +s := undo.New() // unlimited, coalescing on; undo.WithLimit(n), undo.WithCoalescing(false) +s.Push(undo.NewCommand("Write hello", do, undoIt)) +s.Undo() +s.Redo() + +mvvm.BindCommand(s.UndoCommand(), &undoBtn.OnClick, setEnabled) +mvvm.OneWay(s.UndoTextBinding(), &undoBtn.Text, repaint) // "Undo Write hello" +``` + +Una serie de pulsaciones de teclas se fusiona en un solo paso, y un push tras un +deshacer descarta la cola de rehacer. diff --git a/content/es/status.md b/content/es/status.md new file mode 100644 index 0000000..168fd91 --- /dev/null +++ b/content/es/status.md @@ -0,0 +1,66 @@ +--- +title: "Estado" +weight: 60 +description: "Qué se ha demostrado en una pantalla real, un dispositivo real o un terminal real; qué solo se compila; y qué no cubre todavía la CI." +tags: [estado] +--- + +Estas páginas se contrastaron con los módulos en las etiquetas indicadas en +[Módulos]({{< relref "/modules.md" >}}). Cuando el README de un módulo y su +código no coinciden, las páginas siguen al código. + +## Demostrado en vivo {#proven-live} + +Un códec probado dentro del proceso demuestra el códec. Estos son los casos en +los que se interrogó a la propia plataforma: + +| Qué | Dónde se demostró | +|---|---| +| Ventana X11 | CI, bajo Xvfb: un patrón conocido presentado, capturado y muestreado; un clic y una tecla sintetizados con `xdotool` y el `toolkit.Event` despachado comprobado; `Show`/`Hide`/`Raise` releídos desde el servidor | +| Ventana Cocoa | una línea de CI en macOS: una ventana abierta y renderizada, píxeles muestreados, un clic y una tecla sintetizados; `Show`/`Hide`/`Raise` solicitados a un `NSWindow` real desde otra goroutine y releídos desde AppKit | +| Ventana Win32 | una VM Windows 11 arm64: una ventana real que renderiza un `VBox`, un `Label` y un `Button`, con tres clics inyectados que llevan su contador de 0 a 3 a través del procedimiento de ventana | +| Ventana Wayland | frente a un compositor simulado que aplica las reglas de mapeo y desmapeo de xdg-shell | +| Ventana wasmbox | Chromium headless controlando el compositor wasmdesk real: los píxeles compuestos releídos, un clic real encaminado al widget | +| Android | un dispositivo Android 15 arm64: copias limitadas a las zonas dañadas medidas, memoria sucia del memfd medida, el árbol de accesibilidad evaluado en el dispositivo | +| Bandeja de macOS | una sesión macOS real: el icono sale de la barra de menús cuando el programa se detiene, vuelve cuando arranca y abre su menú | +| Bandeja de Linux | CI frente a un bus de sesión real: el nombre se reclama, el menú responde a `GetLayout`, `Quit` lo retira | +| Bandeja de Windows | el icono y su menú en una VM Windows 11 | +| Demos de terminal | un pty real en la CI: bytes de teclas enviados, el fotograma renderizado comprobado | + +## Compilado, no ejecutado {#compiled-not-run} + +- `Show`/`Hide`/`Raise` de Win32: la tabla de decisión tiene pruebas unitarias; + las llamadas en el hilo de la ventana solo se compilan. +- El `Attach` de la bandeja de Windows. +- El bucle DOM de `webcanvas` (`run_js.go`): tras una etiqueta de compilación + `js && wasm`, por lo que queda fuera de la cobertura medida; lo ejercitan las + aplicaciones que lo usan. +- El `Run` nativo de `application`: abrir una ventana real está excluido de su + barrera de cobertura; el contrato, la traducción de eventos y el contador de + disponibilidad están cubiertos. + +## No hecho {#not-done} + +- Las bandejas de Windows y Linux ignoran `MenuItem.Icon`: una fila con icono + se dibuja sin él. +- El backend Wayland de `window` no puede hacer `Raise`: xdg-shell no tiene esa + petición, y xdg-activation necesita un token procedente de la propia entrada + del usuario. +- Android: solo `android/arm64`, porque es el único destino Android que Go + enlaza sin cgo. +- `window.Screens` devuelve `ErrScreensUnsupported` en `js/wasm`. + +## Lo que la CI aún no cubre {#what-ci-does-not-cover-yet} + +Medido a partir de los workflows de cada repositorio en las etiquetas anteriores: + +| Módulo | Carencia | +|---|---| +| `mvvmtk` | sin barrera de cobertura; compilación cruzada solo para amd64 y arm64, en seis sistemas operativos | +| `application` | compilación cruzada para Linux, macOS y Windows solo en amd64 | +| `tray` | compilación cruzada para cuatro pares sistema operativo/arquitectura | +| `mvvmlint` | sin compilación cruzada | +| `bricolint` | su paso «6 arches» compila cinco: falta loong64 | + +`app-template` y `gallery` son aplicaciones de navegador y se compilan para +`js/wasm`, que es su único destino. diff --git a/content/es/surfaces/_index.md b/content/es/surfaces/_index.md new file mode 100644 index 0000000..5d5a540 --- /dev/null +++ b/content/es/surfaces/_index.md @@ -0,0 +1,32 @@ +--- +title: "Dónde pinta" +linkTitle: "Superficies" +weight: 30 +description: "Todas las superficies en las que se puede mostrar un árbol de go-widgets, y qué decide entre ellas." +tags: [superficies, backends] +--- + +| Superficie | Módulo | Painter | +|---|---|---| +| [Una ventana nativa]({{< relref "/surfaces/native-window.md" >}}) en X11, Wayland, macOS, Windows | `window` | píxeles | +| [Una aplicación]({{< relref "/surfaces/application.md" >}}) con un icono en la bandeja y la apariencia del sistema | `application`, `tray` | píxeles | +| [Una pestaña del navegador]({{< relref "/surfaces/browser.md" >}}): un simple `<canvas>` | `webcanvas` | píxeles | +| [Un escritorio en el navegador]({{< relref "/surfaces/browser.md#a-window-on-the-wasmdesk-desktop" >}}): una ventana del compositor wasmdesk | `window` (wasmbox) | píxeles | +| [Un terminal]({{< relref "/surfaces/terminal.md" >}}) | `tui` | celdas | +| [Un APK de Android]({{< relref "/surfaces/android.md" >}}) | `android`, `window` | píxeles | +| [Una imagen]({{< relref "/surfaces/snapshots.md" >}}): SVG o PNG | `svg` | píxeles | + +El árbol de widgets no cambia de una a otra. Lo que cambia es quién posee el +búfer y quién entrega la entrada —un servidor gráfico, un navegador, un +terminal, un host Java—, y cada backend convierte esa entrada en el mismo +`toolkit.Event`. + +## Elegido según lo que hay {#chosen-by-what-is-there} + +`window.Open` no recibe ningún argumento de backend. Examina el entorno —la +plataforma para la que se compiló, `$WAYLAND_DISPLAY`, `$DISPLAY`, el socket +que exporta un host Android— y elige el que puede funcionar. Un binario +compilado para Linux abre una ventana Wayland en una sesión Wayland y una +ventana X11 bajo X; el mismo binario de Android se conecta a su host dentro de +un APK y abre una ventana Linux corriente bajo Termux. Consulte +[cómo elige `Open`]({{< relref "/surfaces/native-window.md#how-open-chooses" >}}). diff --git a/content/es/surfaces/android.md b/content/es/surfaces/android.md new file mode 100644 index 0000000..cb6acf4 --- /dev/null +++ b/content/es/surfaces/android.md @@ -0,0 +1,68 @@ +--- +title: "Android" +linkTitle: "Android" +weight: 50 +description: "go-widgets/android: un APK instalable cuya interfaz entera la organiza y la pinta un proceso Go con CGO_ENABLED=0." +tags: [superficies, android] +--- + +Android es la única plataforma en la que un proceso sin CGO no puede poseer una +ventana: todos los caminos hacia una superficie dibujable pasan por JNI, y JNI +necesita cgo. No hay ningún protocolo de red que hablar en su lugar, como sí +tienen X11 y Wayland. + +Por eso [`go-widgets/android`](https://github.com/go-widgets/android) son dos +procesos: + +| | | +|---|---| +| **Host Java** (`host/`) | posee la `Activity`, el `SurfaceView`, el táctil, las teclas y el ciclo de vida. Copia píxeles; no sabe nada de widgets. | +| **Aplicación Go** (`cmd/gwapp`) | un ejecutable `CGO_ENABLED=0 GOOS=android` corriente. Posee el layout, los widgets, el tema, la detección de impactos y el foco, sin cambios respecto a cualquier otro backend. | + +El host Java ocupa el lugar que ocupa el servidor X en Linux. Los píxeles +viajan a través de un **memfd** que la aplicación crea y entrega al host por el +socket; el lado Go escribe RGBA_8888, que es byte a byte el ARGB_8888 de +Android, así que la copia se hace sin conversión y solo del rectángulo dañado. +La entrada, los márgenes de inserción, el texto del IME y el árbol de +accesibilidad atraviesan un protocolo por tramas sobre un `LocalSocket` +abstracto. + +```go +c, err := android.Dial("my app", nil) // nil theme: toolkit.DefaultDark() +if errors.Is(err, android.ErrUnsupported) { + return nil // not under a host +} +defer c.Close() +return c.Run(myWidgetTree()) +``` + +`Client` satisface el `Backend` de `window`, de modo que una aplicación pasa de +Android a los backends de escritorio sin cambiar una línea por encima de la +ventana, y [`window.Open`]({{< relref "/surfaces/native-window.md#how-open-chooses" >}}) +lo elige por sí solo cuando el socket del host está presente. + +## Solo arm64 {#arm64-only} + +`android/arm64` es el único destino Android que Go enlaza sin cgo; `arm`, +`amd64` y `386` requieren enlazado externo. La CI comprueba ambas mitades —el +que compila y los tres que no—, de modo que el día en que Go levante la +restricción, la compilación lo dirá. + +## Accesibilidad {#accessibility} + +Un lector de pantalla vería el `SurfaceView` como un único rectángulo opaco. En +su lugar, el host le ofrece una jerarquía de vistas virtual: un nodo por cada +widget accesible, con la clase `android.widget.*` a partir de la cual Android +elige sus anuncios, el texto, los límites y una acción de activación. El árbol +se **extrae** cuando algo lo lee, nunca se envía por iniciativa propia, así que +una aplicación sin ningún servicio de accesibilidad conectado nunca lo +construye. Una activación vuelve como un clic corriente en el centro del +elemento, por el mismo código que recorre un toque. + +## Táctil y animación {#touch-and-animation} + +Cada muestra táctil llega al árbol como un evento táctil y luego como un evento +de ratón: el primero para los widgets que reconocen gestos, el segundo para los +muchos que solo escuchan clics. Los widgets animados avanzan en un bucle de +fotogramas que empieza cuando algo comienza a animarse y se detiene cuando +nada lo hace. diff --git a/content/es/surfaces/application.md b/content/es/surfaces/application.md new file mode 100644 index 0000000..82c8a17 --- /dev/null +++ b/content/es/surfaces/application.md @@ -0,0 +1,85 @@ +--- +title: "Una aplicación y su bandeja" +linkTitle: "Aplicación y bandeja" +weight: 20 +description: "go-widgets/application gestiona el ciclo de vida por encima de una ventana; go-widgets/tray coloca un icono y un menú en la barra de menús o en el área de notificación." +tags: [superficies, aplicación, bandeja] +--- + +## La aplicación {#the-application} + +[`go-widgets/application`](https://github.com/go-widgets/application) es la +capa situada por encima de [`window`]({{< relref "/surfaces/native-window.md" >}}): +abre la ventana, hace girar su bucle, coloca un icono en la bandeja en cuanto +el primer fotograma está listo, pasa a la aplicación la apariencia del sistema +en vivo (oscura o clara, color de acento, fuente del sistema), publica el árbol +de accesibilidad y llama a `onReady` en cuanto el primer fotograma está en +pantalla. + +```go +spec := application.Spec{ + Name: "News Reader", + Identifier: "com.example.reader", + Version: "1.0.0", + Tray: func() *tray.Menu { + return tray.NewMenu().Add( + tray.Item("Refresh", refresh), + tray.Item("Quit", quit), + ) + }, +} +cfg := application.Config{Title: "News Reader", Width: 1200, Height: 800} +err := application.Run(spec, cfg, handler, func() { log.Println("on screen") }) +``` + +`handler` implementa `Handler` —`Frame`, `Resize`, eventos de ratón, de +desplazamiento y de teclado— y puede implementar cualquiera de las capacidades +opcionales: apariencia, atajos, clics y teclas con modificadores, clic +secundario, menús contextuales, portapapeles, accesibilidad, controles nativos. +El bucle nunca envuelve el manejador; hace una aserción de tipo para cada +capacidad y respeta las que encuentra. Una prueba recorre el paquete en busca de +esas aserciones y falla si la documentación del paquete no las nombra todas. + +Una bandeja que no puede colocarse es un error de `Run`, no un silencio: +`errors.Is(err, tray.ErrNoBackend)` dice por qué. Una aplicación que quiere su +ventana pase lo que pase con la bandeja deja `Spec.Tray` a nil y ejecuta una +por su cuenta. + +`Run` cierra su ventana cuando el bucle termina, de modo que el «Open» de una +aplicación de bandeja puede volver a llamar a `Run` para obtener una ventana +nueva. Para un host que no es una ventana —un shell de escritorio, una pestaña +del navegador—, `Bind` y `BindScaled` devuelven un `*toolkit.Surface` +conectado al mismo manejador. + +## La bandeja {#the-tray} + +Un icono de bandeja es integración con el sistema operativo, no un widget +pintado, así que [`go-widgets/tray`](https://github.com/go-widgets/tray) usa la +API propia de cada plataforma, siempre con `CGO_ENABLED=0`: + +| Plataforma | API | A través de | +|---|---|---| +| macOS | `NSStatusItem`, `NSMenu` | purego y el runtime de Objective-C | +| Windows | `Shell_NotifyIcon`, `TrackPopupMenu` | `golang.org/x/sys/windows` | +| Linux | `StatusNotifierItem`, `com.canonical.dbusmenu` | DBus, en Go | + +```go +menu := tray.NewMenu().Add( + tray.Item("Open", open), + tray.Checkbox("Notifications", true, setNotify), + tray.SubMenu("Recent", tray.NewMenu().Add(tray.Item("file.txt", nil))), + tray.Separator(), + tray.Item("Quit", func() { t.Quit() }), +) +t := tray.New(iconPNG).SetTooltip("My App").SetMenu(menu) +t.Run() // blocks on the platform's loop until Quit +``` + +Un programa que ya ejecuta el bucle de una ventana no puede ceder a `Run` su +hilo principal: `Attach` coloca el mismo icono y retorna, y `Quit` lo retira. + +Los backends nativos están activados por defecto; no hay ninguna etiqueta de +compilación que recordar. En una plataforma sin ninguno, `Run` devuelve +`ErrNoBackend`: la diferencia entre «aquí no hay bandeja» y una bandeja que en +silencio no hace nada. Una prueba o un servicio sin interfaz pasa +`WithBackend(tray.NewHeadless())`. diff --git a/content/es/surfaces/browser.md b/content/es/surfaces/browser.md new file mode 100644 index 0000000..e603436 --- /dev/null +++ b/content/es/surfaces/browser.md @@ -0,0 +1,56 @@ +--- +title: "Una pestaña del navegador" +linkTitle: "Navegador" +weight: 30 +description: "Ejecutar un árbol de widgets en un simple <canvas> con go-widgets/webcanvas, o como una ventana del compositor wasmdesk." +tags: [superficies, wasm, navegador, webcanvas, wasmdesk] +--- + +El toolkit es Go puro sin dependencia del DOM, así que `GOOS=js GOARCH=wasm` lo +compila como cualquier otro destino. Hay dos maneras de poner el resultado +delante de alguien: + +## Un simple canvas {#a-plain-canvas} + +[`go-widgets/webcanvas`](https://github.com/go-widgets/webcanvas) copia un +framebuffer RGBA en un `<canvas>` y devuelve a la escena los eventos de puntero +y de teclado del DOM. Sin compositor, sin `SharedArrayBuffer`, sin cabeceras de +aislamiento de origen cruzado: cualquier alojamiento estático lo sirve. + +```go +//go:build js && wasm + +package main + +import "github.com/go-widgets/webcanvas" + +func main() { webcanvas.Run("screen", myScene()) } +``` + +La escena implementa `App`: `Size`, `Draw(buf []byte)` y un método por tipo de +evento, cada uno indicando si algo ha cambiado para que un fotograma sin +cambios no se repinte. También puede implementar `Ticker`, `Animator`, +`Resizer` o `Scroller`. + +La [galería](https://go-widgets.github.io/gallery/) y +[`app-template`]({{< relref "/getting-started.md" >}}) funcionan así. + +## Una ventana en el escritorio wasmdesk {#a-window-on-the-wasmdesk-desktop} + +En `js/wasm`, [`window.Open`]({{< relref "/surfaces/native-window.md" >}}) +devuelve un cliente del compositor de navegador +[wasmdesk/wasmbox](https://github.com/wasmdesk/wasmbox). Asigna su superficie +en un `SharedArrayBuffer`, dice `hello` por su `MessagePort`, espera `welcome`, +pinta en el búfer compartido y envía `commit`: la superficie entera, o solo los +rectángulos dañados. Los mensajes `input` del compositor se convierten en +`toolkit.Event`, como en X11. + +Así, una aplicación escrita contra `window` funciona sin cambios como ventana +nativa y como ventana de un escritorio en una pestaña del navegador. La +[demo del shell de escritorio](https://github.com/go-widgets/desktop) hace +exactamente eso, a partir de un solo código fuente. + +El códec del protocolo está cubierto al 100 % en todas las plataformas. La +prueba en vivo ejecuta Chromium headless contra el compositor wasmdesk real: +lanza el cliente, lee los píxeles compuestos por el propio compositor, hace clic +y comprueba que el clic ha llegado al widget. diff --git a/content/es/surfaces/native-window.md b/content/es/surfaces/native-window.md new file mode 100644 index 0000000..8990437 --- /dev/null +++ b/content/es/surfaces/native-window.md @@ -0,0 +1,94 @@ +--- +title: "Una ventana nativa" +linkTitle: "Ventana nativa" +weight: 10 +description: "go-widgets/window: una sola API Open/Run sobre X11, Wayland, Cocoa, Win32, GTK4, Android y wasmbox, todo con CGO_ENABLED=0." +tags: [superficies, ventana, x11, wayland, macos, windows] +--- + +[`go-widgets/window`](https://github.com/go-widgets/window) abre una ventana +real y ejecuta en ella un árbol de widgets: layout, dibujo, presentación, +traducción de la entrada a `toolkit.Event`, y vuelta a empezar hasta que la +ventana se cierra. + +```go +w, err := window.Open(window.Config{Title: "Demo"}) +if err != nil { + return err +} +defer w.Close() +return w.Run(root) // root is any toolkit.Widget +``` + +`Config` recibe además el tamaño inicial en **puntos lógicos** (cero pide un +tamaño por defecto legible), la instancia y la clase de `WM_CLASS`, un `Theme` +y un `RenderScale`. + +## Cómo elige Open {#how-open-chooses} + +| Plataforma y entorno | Backend | Cómo llega a la pantalla | +|---|---|---| +| Linux, `$GO_WIDGETS_GTK` definida | GTK4 | GTK posee la ventana; el framebuffer es un `GtkPicture`, y los controles nativos son widgets GTK reales por encima de él. Necesita el runtime de libgtk-4. | +| Linux, `$WAYLAND_DISPLAY` definida | Wayland | xdg-shell sobre el socket unix del compositor | +| Linux, en otro caso `$DISPLAY` | X11 | el protocolo central de X11 sobre el socket unix, con MIT-SHM | +| macOS | Cocoa | `NSWindow` y `NSView` a través de [go-macos/objc](https://github.com/go-macos/objc) (purego) | +| Windows | Win32 | un `HWND` de nivel superior mediante llamadas al sistema de user32 y gdi32, `StretchDIBits` | +| Android, `$GW_ANDROID_SOCKET` definida | Host Android | un protocolo por tramas hacia [el host Java]({{< relref "/surfaces/android.md" >}}), píxeles en un memfd compartido | +| Android, en otro caso | Wayland o X11 | un shell bajo Termux sigue teniendo un servidor gráfico al que conectarse | +| `js/wasm` | wasmbox | un cliente del [compositor wasmdesk]({{< relref "/surfaces/browser.md#a-window-on-the-wasmdesk-desktop" >}}) | +| cualquier otra cosa | — | `window.ErrUnsupported`, para que una compilación cruzada siga compilando | + +Todos los backends son `CGO_ENABLED=0`. El de X11 es el protocolo central +escrito desde cero sobre el socket —sin Xlib, sin XCB—, con ambos órdenes de +bytes, la cookie de Xauthority, el mapeo de keysyms, `PutImage` troceado por +debajo del tamaño máximo de petición del servidor y la vía rápida MIT-SHM, con +el segmento compartido pasado por `SCM_RIGHTS`. A Windows se llega a través de +las propias DLL del proceso con `syscall.NewLazyDLL` y un procedimiento de +ventana `syscall.NewCallback`; a macOS, a través del runtime de Objective-C con +purego. + +La parte de cada backend que no depende de la plataforma —mapeo de eventos, +cálculo de coordenadas, empaquetado de píxeles, rectángulos dañados— vive en un +códec cubierto al 100 %, en todos los sistemas operativos. La fina capa de +unión con la plataforma se demuestra en vivo: consulte +[Estado]({{< relref "/status.md" >}}). + +## Solo lo que ha cambiado {#only-what-changed} + +Una raíz que implementa `DamageRenderer` (como hace `toolkit/scene.HostRoot`) +informa de los rectángulos que ha repintado, y los backends Cocoa, Win32 y +wasmbox presentan solo esos. + +## HiDPI {#hidpi} + +Por defecto hay un píxel de framebuffer por punto lógico: la interfaz se +organiza y se pinta a un tamaño legible, y el compositor la sobremuestrea. En +Windows, la ventana declara compatibilidad con DPI por monitor y el sistema +operativo escala el fotograma lógico al área cliente física. `RenderScale` pide +un framebuffer a la resolución real del panel, lo cual solo es correcto para una +raíz que organiza su layout en píxeles del dispositivo. + +## Pantallas, y mostrar y ocultar {#screens-and-showing-and-hiding} + +`window.Screens()` enumera las pantallas conectadas, la principal primero, en +puntos lógicos y descontando los paneles del escritorio, y puede llamarse antes +de `Open`. El nombre de una pantalla es el del propio panel (`"DELL U2720Q"`, +tomado de su EDID), y recurre al conector (`"HDMI-1"`) cuando el panel no +publica ninguno, y también cuando dos paneles conectados publican el mismo, +porque un nombre que no permite distinguirlos no es un nombre. + +`window.Show`, `Hide` y `Raise` retiran de la pantalla una ventana abierta sin +cerrarla y la traen de vuelta: lo que necesita una +[aplicación de bandeja]({{< relref "/surfaces/application.md#the-tray" >}}). + +| Backend | Show | Hide | Raise | +|---|---|---|---| +| X11 | `MapWindow` | retirada ICCCM, para que salga de la barra de tareas | mapear, elevar, `_NET_ACTIVE_WINDOW` | +| Wayland | volver a hacer commit | búfer nulo y commit | `ErrNotSupported`: xdg-shell no tiene esa petición | +| macOS | `orderFront:` | `orderOut:` | activar la aplicación, `makeKeyAndOrderFront:` | +| Windows | `SW_SHOWNA` | `SW_HIDE` | `SW_RESTORE` y `SetForegroundWindow` | +| GTK, Android, wasmbox | `ErrNotSupported` | `ErrNotSupported` | `ErrNotSupported` | + +Conceder el foco corresponde a la plataforma: con la prevención del robo de +foco, `Raise` puede limitarse a marcar la ventana como necesitada de atención, +y no puede saberlo. diff --git a/content/es/surfaces/snapshots.md b/content/es/surfaces/snapshots.md new file mode 100644 index 0000000..207c64c --- /dev/null +++ b/content/es/surfaces/snapshots.md @@ -0,0 +1,32 @@ +--- +title: "Instantáneas: un widget como SVG o PNG" +linkTitle: "Instantáneas" +weight: 60 +description: "go-widgets/svg envuelve cualquier renderizado RGBA en un SVG autocontenido, exacto bit a bit, o lo escribe como PNG." +tags: [superficies, svg, png] +--- + +[`go-widgets/svg`](https://github.com/go-widgets/svg) convierte un búfer RGBA en +un SVG autocontenido: un `viewBox` fijado al tamaño en píxeles del renderizado, +y los píxeles como un PNG en base 64 bajo `image-rendering: pixelated`. Cada +píxel mostrado es el píxel que dibujó el widget —sin revectorización, sin +sustitución de fuentes— y se mantiene nítido a cualquier nivel de zoom. + +```go +// any RGBA producer +svg.Snapshot(f, surface, w, h, "my image") + +// a toolkit widget +btn := toolkit.NewButton("Click me", nil) +btn.SetBounds(toolkit.Rect{W: 200, H: 40}) +widget.Snapshot(f, btn, 200, 40, toolkit.DefaultLight(), "widget: button") +widget.PNG(p, btn, 200, 40, toolkit.DefaultLight()) +``` + +La etiqueta se convierte en el `<title>` y el `aria-label` del SVG, escapada. El +paquete raíz no depende de nada; el subpaquete `widget` está separado para que +un consumidor con su propio productor de píxeles no arrastre el toolkit a su +grafo de módulos. + +`svg/cmd/gallery-render` renderiza el catálogo de widgets, en claro y en oscuro: +[go-widgets.github.io/svg](https://go-widgets.github.io/svg/). diff --git a/content/es/surfaces/terminal.md b/content/es/surfaces/terminal.md new file mode 100644 index 0000000..f2badc1 --- /dev/null +++ b/content/es/surfaces/terminal.md @@ -0,0 +1,68 @@ +--- +title: "Un terminal" +linkTitle: "Terminal" +weight: 40 +description: "go-widgets/tui renderiza widgets a través del painter de celdas: un renderizador de instantáneas, un ejecutor interactivo y widgets dimensionados para una cuadrícula de caracteres." +tags: [superficies, tui, terminal] +--- + +[`go-widgets/tui`](https://github.com/go-widgets/tui) convierte un árbol de +widgets en un fotograma de terminal a través del +[painter de celdas]({{< relref "/rendering/painter.md" >}}). +Solo la biblioteca estándar: sin `x/term`, sin cgo. + +## Un fotograma {#one-frame} + +```go +_ = tui.RenderOnce(os.Stdout, widgets, nil) // nil theme: light +``` + +`RenderOnceSized(w, cols, rows, widgets, theme)` recibe el tamaño de forma +explícita; `RenderOnce` lee `COLUMNS` y `LINES` y, en su defecto, usa 80×24. +Leer el entorno en lugar de llamar a `TIOCGWINSZ` mantiene el paquete libre de +código específico de cada plataforma. + +## Un programa interactivo {#an-interactive-program} + +```go +app := tui.NewApp() +app.Root = buildScene() +app.Keys["q"] = func(a *tui.App) { a.Quit() } +app.Keys["Ctrl+C"] = func(a *tui.App) { a.Quit() } +os.Exit(app.Run()) +``` + +`Run` entra en la pantalla alternativa y en modo raw, despacha la entrada a +`Root` (primero los manejadores de teclas globales), sigue `SIGWINCH` y siempre +restaura el terminal, también ante un panic. Un manejador de teclas llama a +`Consume` para que un evento no llegue a `Root`; `InputTarget` envía todos los +eventos no consumidos a un único widget, que es como una paleta de comandos se +traga lo que se teclea mientras está abierta. `NewFocusRing` da a un formulario +el recorrido con Tab y Mayús+Tab. + +## Widgets dimensionados para celdas {#widgets-sized-for-cells} + +La mayoría de los widgets del toolkit están diseñados en píxeles: sus +constantes de relleno cuentan celdas bajo el painter de celdas y se inflan. Por +eso `tui` tiene su propio conjunto **nativo de celdas** —`TextEditor`, `Entry`, +`Button`, `CheckButton`, `RadioButton`, `Scale`, `ProgressBar`, `Sparkline`, +`ListBox`, `TreeView`, `Table`, `MenuBar`, `Notebook`, `HSplit`, `VSplit`, +`Dialog`, `Dropdown`, `Spinner` y más—, donde un glifo es una celda. Cada uno +sigue siendo un `toolkit.Widget`, así que la misma instancia también dibuja en +píxeles. + +`TextEditor` es un editor de código de lectura y escritura: margen de números, +deshacer y rehacer, buscar y reemplazar, selección, desplazamiento de líneas y +resaltado de sintaxis para Go (mediante `go/scanner`), JavaScript y TypeScript, +Python, Ruby, shell, C y C++, Rust, JSON, YAML, HCL, TOML, LaTeX y Markdown, sin +ninguna dependencia externa. + +```sh +go run ./cmd/tui-widgets | less -R # every cell-native widget, one per slot +go run ./cmd/tui-widget-explorer # the same, interactive +go run ./cmd/tui-explorer # a file browser +go run ./cmd/tui-editor --file=x.go # a modal editor +``` + +Los dos programas de demostración se prueban de extremo a extremo en un pty +real: bytes de teclas reales de entrada, y el fotograma renderizado comprobado. diff --git a/content/fr/_index.md b/content/fr/_index.md new file mode 100644 index 0000000..835b3ee --- /dev/null +++ b/content/fr/_index.md @@ -0,0 +1,76 @@ +--- +title: "go-widgets" +linkTitle: "Accueil" +type: docs +cascade: + type: docs +description: "Une boîte à outils de widgets en Go pur, et toutes les surfaces sur lesquelles elle peint : une fenêtre native sur X11, Wayland, macOS et Windows, un canvas de navigateur, un terminal, un APK Android." +--- + +**Un seul arbre de widgets, peint par Go, sur toutes les surfaces que Go atteint.** Une +fenêtre native sur X11, Wayland, macOS et Windows ; un `<canvas>` dans un onglet de +navigateur ; un terminal ; un APK Android. Pas de chaîne de compilation C, pas de webview, pas +de jeu de widgets du système : chaque module se compile avec `CGO_ENABLED=0`, et chaque pixel +est dessiné par la boîte à outils. + +```go +w, err := window.Open(window.Config{Title: "Hello"}) +if err != nil { + return err +} +defer w.Close() + +box := toolkit.NewVBox() +box.Append(toolkit.NewLabel("Hello from go-widgets")) +box.Append(toolkit.NewButton("Click me", func() { /* ... */ })) +return w.Run(box) // an X11 or Wayland window, an NSWindow, a Win32 window +``` + +C'est +[`cmd/windowdemo`](https://github.com/go-widgets/window/blob/main/cmd/windowdemo/main.go), +raccourci. Le même `box` dessiné dans un navigateur, un terminal ou un PNG, c'est le même +code avec une autre surface en dessous. + +## Pourquoi une seule boîte à outils dessine tout {#why-one-toolkit-draws-everything} + +Un widget qui appelle le bouton propre à la plateforme n'est pas plus portable que la +plateforme la moins capable qu'il vise, et son apparence change sur chacune. go-widgets prend +le chemin inverse : la méthode `Draw` d'un widget ne parle jamais qu'à un +[`painter.Painter`]({{< relref "/rendering/painter.md" >}}), une poignée de +primitives (remplir un rectangle, en tracer le contour, poser un pixel, dessiner du texte). Que +ces appels deviennent des pixels RGBA ou des cellules de terminal, c'est l'affaire du back-end, +pas celle du widget. + +Ce que cela coûte est dit franchement : pas d'apparence native par défaut, et pas +d'accessibilité gratuite — la boîte à outils publie son propre +[arbre d'accessibilité]({{< relref "/rendering/toolkit.md#accessibility" >}}) +vers chaque plateforme. Ce que cela apporte : un seul jeu de widgets, testé une fois, au +comportement identique partout, et un binaire qui se compile de manière croisée depuis +n'importe quelle machine vers chacune d'elles. + +## Pour aller plus loin {#where-to-go-next} + +| | | +|---|---| +| [Une première application]({{< relref "/getting-started.md" >}}) | depuis `app-template`, ou depuis une simple fenêtre | +| [Comment un widget atteint l'écran]({{< relref "/rendering/_index.md" >}}) | la jonction du painter, le jeu de widgets, les skins | +| [La jonction du painter]({{< relref "/rendering/painter.md" >}}) | les primitives avec lesquelles chaque widget est dessiné | +| [La boîte à outils]({{< relref "/rendering/toolkit.md" >}}) | 160 types de widgets, thèmes, mises en page, texte, accessibilité | +| [Skins]({{< relref "/rendering/skin.md" >}}) | les parties et les états d'un widget dans un fichier de données | +| [L'état vit dans un view-model]({{< relref "/state/_index.md" >}}) | pourquoi on n'affecte pas les champs d'un widget à la main | +| [MVVM]({{< relref "/state/mvvm.md" >}}) | observables, commandes, binders, annulation | +| [La colonne vertébrale des données]({{< relref "/state/data.md" >}}) | enregistrements typés, requêtes, stockage local ou distant | +| [Où elle peint]({{< relref "/surfaces/_index.md" >}}) | chaque back-end et la façon dont il est choisi | +| [Une fenêtre native]({{< relref "/surfaces/native-window.md" >}}) | X11, Wayland, Cocoa, Win32, GTK4, Android, wasmbox | +| [Une application et son icône de zone de notification]({{< relref "/surfaces/application.md" >}}) | cycle de vie, apparence, une icône dans la barre des menus | +| [Un onglet de navigateur]({{< relref "/surfaces/browser.md" >}}) | un simple `<canvas>`, ou une fenêtre wasmdesk | +| [Un terminal]({{< relref "/surfaces/terminal.md" >}}) | des widgets natifs en cellules et un exécuteur interactif | +| [Android]({{< relref "/surfaces/android.md" >}}) | un vrai APK peint par un processus sans CGO | +| [Instantanés]({{< relref "/surfaces/snapshots.md" >}}) | un widget sous forme de SVG ou de PNG | +| [Règles imposées par la CI]({{< relref "/guard-rails/_index.md" >}}) | `mvvmlint` et `bricolint` | +| [Modules]({{< relref "/modules.md" >}}) | les dix-neuf, avec leurs sources et leurs références | +| [État]({{< relref "/status.md" >}}) | ce qui est vérifié, où, et ce qui n'est pas fait | + +## Licence {#licence} + +BSD-3-Clause. diff --git a/content/fr/getting-started.md b/content/fr/getting-started.md new file mode 100644 index 0000000..8998621 --- /dev/null +++ b/content/fr/getting-started.md @@ -0,0 +1,79 @@ +--- +title: "Une première application" +weight: 5 +description: "Partir d'app-template, déjà conforme à MVVM et au vert, ou ouvrir une simple fenêtre et y ajouter des widgets." +tags: [premiers pas, app-template, window] +--- + +## Depuis le modèle {#from-the-template} + +[`go-widgets/app-template`](https://github.com/go-widgets/app-template) est un +dépôt modèle : une petite mais vraie application de navigateur, avec un champ de +recherche, un filtre par catégorie, la liste des lignes qui correspondent et une ligne d'état. +Tout ce dont une application plus grande a besoin est déjà branché : un view-model qui ne +contient aucun widget, une vue liée à celui-ci, un hôte wasm, une barrière de couverture à 100 %, et la +[barrière MVVM]({{< relref "/guard-rails/mvvmlint.md" >}}). + +```sh +git clone https://github.com/go-widgets/app-template my-app +cd my-app +go mod edit -module github.com/you/my-app +./build.sh # dist/app.wasm, dist/wasm_exec.js, dist/index.html +``` + +Servez `dist/` en HTTP : une page ouverte depuis `file://` ne peut pas instancier de wasm. + +| Fichier | Ce qu'il contient | +|---|---| +| `viewmodel.go` | tout l'état, sous forme d'observables et de commandes [`mvvm`]({{< relref "/state/mvvm.md" >}}). Aucun widget. Testable sans canvas. | +| `scene.go` | les widgets, chacun lié au view-model par `mvvmtk`. Il n'affecte jamais lui-même un champ d'état d'un widget. | +| `main.go` | le seul fichier avec une étiquette de compilation (`js && wasm`) : il transmet les entrées à la boîte à outils et copie l'image dans un `<canvas>`. | + +Modifiez les deux premiers et faites passer l'état par les binders : la tâche +`mvvm` de la CI fait échouer une pull request qui affecte directement un champ d'un widget. + +## Depuis une simple fenêtre {#from-a-bare-window} + +```go +package main + +import ( + "github.com/go-widgets/toolkit" + "github.com/go-widgets/window" +) + +func main() { + w, err := window.Open(window.Config{Title: "Demo"}) + if err == window.ErrUnsupported { + return // no native back-end on this platform + } + if err != nil { + panic(err) + } + defer w.Close() + + box := toolkit.NewVBox() + box.Append(toolkit.NewLabel("Hello")) + box.Append(toolkit.NewButton("Click me", func() {})) + w.Run(box) // layout, draw, present, dispatch input, until closed +} +``` + +Une largeur et une hauteur laissées à zéro demandent au back-end une taille lisible par défaut ; sous +macOS, c'est une fraction de l'écran principal. `CGO_ENABLED=0 go build` produit +le binaire pour chaque plateforme, depuis n'importe quelle machine. + +## Quel module pour quoi {#which-module-for-what} + +| Vous voulez | Commencez par | +|---|---| +| une fenêtre de bureau | [`window`]({{< relref "/surfaces/native-window.md" >}}) | +| une application de bureau avec une icône de zone de notification et l'apparence du système | [`application`]({{< relref "/surfaces/application.md" >}}) | +| une page dans un navigateur | [`webcanvas`]({{< relref "/surfaces/browser.md" >}}), ou le modèle ci-dessus | +| un programme en terminal | [`tui`]({{< relref "/surfaces/terminal.md" >}}) | +| une application Android | [`android`]({{< relref "/surfaces/android.md" >}}) | +| une image d'un widget pour un README | [`svg`]({{< relref "/surfaces/snapshots.md" >}}) | + +Pour voir le jeu de widgets avant d'écrire quoi que ce soit, ouvrez la +[galerie](https://go-widgets.github.io/gallery/) : chaque famille de widgets, en direct, +dans un canvas de navigateur. diff --git a/content/fr/guard-rails/_index.md b/content/fr/guard-rails/_index.md new file mode 100644 index 0000000..9764c3e --- /dev/null +++ b/content/fr/guard-rails/_index.md @@ -0,0 +1,24 @@ +--- +title: "Règles imposées par la CI" +linkTitle: "Règles de CI" +weight: 40 +description: "Deux contrôles go/analysis qu'une application exécute comme vérifications de statut obligatoires : l'état passe par MVVM, et pas d'interface dessinée à la main." +tags: [ci, lint] +--- + +Deux conventions gardent honnête l'interface d'une application, et toutes deux s'éroderaient +un raccourci commode après l'autre si elles étaient seulement écrites quelque part. Chacune est donc un +analyseur [`go/analysis`](https://pkg.go.dev/golang.org/x/tools/go/analysis), +exécuté via `go vet`, avec un workflow réutilisable qu'une application marque comme vérification +**obligatoire**. + +| | Refuse | +|---|---| +| [`mvvmlint`]({{< relref "/guard-rails/mvvmlint.md" >}}) | l'affectation à la main d'un champ d'état d'un widget, et un paquet qui a des widgets mais pas de view-model | +| [`bricolint`]({{< relref "/guard-rails/bricolint.md" >}}) | les primitives du painter dans le code applicatif, et un widget reconstruit à chaque image | + +Tous deux sont prudents par construction : une règle ne se déclenche que lorsque le **type +statique** du récepteur se résout, via `go/types`, en un type de la boîte à outils ou du painter. +Un champ ou une méthode de même nom sur un type sans rapport n'est jamais signalé. Les +exceptions voulues sont des directives explicites dans le code, si bien que chacune est +écrite là où elle est faite. diff --git a/content/fr/guard-rails/bricolint.md b/content/fr/guard-rails/bricolint.md new file mode 100644 index 0000000..cf15621 --- /dev/null +++ b/content/fr/guard-rails/bricolint.md @@ -0,0 +1,53 @@ +--- +title: "bricolint : pas d'interface dessinée à la main" +linkTitle: "bricolint" +weight: 20 +description: "Fait échouer une pull request qui peint des éléments d'interface avec des primitives du painter dans le code applicatif, ou qui reconstruit un widget à chaque image." +tags: [ci, lint, painter] +--- + +Le *bricolage* — un dessin improvisé, fait main — est la façon dont une interface perd sans bruit +son retour visuel d'appui, de survol et de focus, son thème, sa mise à l'échelle HiDPI +et son accessibilité. [`go-widgets/bricolint`](https://github.com/go-widgets/bricolint) +empêche une application passée à la boîte à outils de revenir en arrière. + +```go +p.FillRect(bar, theme.Bg) // flagged: hand-drawn chrome +toolkit.NewBackdrop(theme.Bg).Draw(p) // a toolkit widget + +func (v *view) Draw(p painter.Painter) { + b := toolkit.NewButton("ok") // flagged: a new widget every frame + b.Draw(p) +} +``` + +1. **Une primitive du painter dans le code applicatif** : une méthode de dessin (`FillRect`, + `StrokeRect`, `FillPath`, `DrawImage`, `Text`, `PutPixel`, …) appelée sur un + récepteur dont le type statique est un type du painter. Interroger ou découper la + surface (`Size`, `PushClip`, `PushTranslate`) n'est pas dessiner et n'est pas + signalé. `-primitives=` remplace la liste. +2. **Un widget jeté à chaque image** : un appel `toolkit.New…` dans une méthode + nommée `Draw`, `Paint` ou `Render`. Un widget reconstruit à chaque peinture ne garde aucun + état d'interaction. Construisez-le une fois, gardez-le dans un champ, pilotez-le par une + liaison. `-checkthrow=false` désactive ce contrôle. + +Certain code est une véritable feuille — un framebuffer de jeu, un back-end de painter, les +entrailles de la boîte à outils elle-même — et le dit explicitement : + +```go +p.FillRect(bg, c) //bricolint:allow engine SVG raster blit — a genuine leaf +``` + +```go +//bricolint:allowfile painter back-end — this file IS the leaf +package pdfsurface +``` + +**La raison est obligatoire** : une directive qui n'en donne pas est ignorée, et le +signalement continue de se déclencher jusqu'à ce que quelqu'un écrive la justification. + +```yaml +jobs: + bricolint: + uses: go-widgets/bricolint/.github/workflows/bricolint.yml@main +``` diff --git a/content/fr/guard-rails/mvvmlint.md b/content/fr/guard-rails/mvvmlint.md new file mode 100644 index 0000000..84bc793 --- /dev/null +++ b/content/fr/guard-rails/mvvmlint.md @@ -0,0 +1,44 @@ +--- +title: "mvvmlint : l'état passe par le view-model" +linkTitle: "mvvmlint" +weight: 10 +description: "Fait échouer une pull request qui affecte directement un champ d'état d'un widget de la boîte à outils, ou qui a des widgets et pas de view-model." +tags: [ci, lint, mvvm] +--- + +[`go-widgets/mvvmlint`](https://github.com/go-widgets/mvvmlint) ne se déclenche que dans +les paquets qui importent la boîte à outils. + +1. **Un champ d'état d'un widget affecté à la main.** `entry.Text = "x"`, où le + type statique de `entry` est un widget de la boîte à outils et `Text` un champ d'état, est + signalé. `&entry.Text` — ce que prennent les [binders]({{< relref "/state/mvvm.md" >}}) + — n'est pas une affectation et n'est jamais signalé ; pas plus qu'un littéral + composite (`toolkit.Entry{Text: "x"}`), ni la composition d'un callback tel que + `OnChange`. +2. **Des widgets et pas de view-model.** Un paquet qui importe la boîte à outils mais pas + `go-widgets/mvvm` reçoit un diagnostic. `-requirevm=false` désactive ce contrôle pour + une bibliothèque de widgets qui, légitimement, n'a pas de view-model. + +Les champs d'état sont ceux des widgets de la boîte à outils (`Text`, `Items`, `Rows`, +`Root`, `Selected`, `Value`, `Checked`, …) ; `-statefields=` remplace la liste. +Un fichier nommé `*_binding.go`, ou contenant `//mvvmlint:allow`, est exempté de +la première règle. + +```sh +go install github.com/go-widgets/mvvmlint/cmd/mvvmlint@latest +go vet -vettool="$(go env GOPATH)/bin/mvvmlint" ./... +``` + +```yaml +# .github/workflows/mvvm.yml +name: mvvm +on: + pull_request: + push: + branches: [main] +jobs: + mvvm: + uses: go-widgets/mvvmlint/.github/workflows/mvvmlint.yml@main +``` + +[`app-template`]({{< relref "/getting-started.md" >}}) est livré avec cette tâche. diff --git a/content/fr/modules.md b/content/fr/modules.md new file mode 100644 index 0000000..b7ce697 --- /dev/null +++ b/content/fr/modules.md @@ -0,0 +1,47 @@ +--- +title: "Modules" +weight: 50 +description: "Les dix-neuf modules de go-widgets, le rôle de chacun, et où se trouvent sa source et sa référence d'API." +tags: [modules] +--- + +La référence d'API est générée à partir de la source par +[pkg.go.dev](https://pkg.go.dev/), qui suit les étiquettes publiées ; ces +pages expliquent, et ne la recopient pas. L'étiquette est la plus récente au moment de la +rédaction — la liste des étiquettes du dépôt lui-même fait foi. + +| Module | Étiquette | Ce que c'est | Référence | +|---|---|---|---| +| [`painter`](https://github.com/go-widgets/painter) | v0.15.0 | [la jonction de dessin]({{< relref "/rendering/painter.md" >}}) : painters en pixels et en cellules | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/painter) | +| [`toolkit`](https://github.com/go-widgets/toolkit) | v0.328.0 | [le jeu de widgets]({{< relref "/rendering/toolkit.md" >}}), thèmes, mises en page, texte | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/toolkit) | +| [`skin`](https://github.com/go-widgets/skin) | v0.2.0 | [des widgets décrits par des données]({{< relref "/rendering/skin.md" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/skin) | +| [`isoicons`](https://github.com/go-widgets/isoicons) | v0.3.0 | jeux d'icônes isométriques (cloud-native, AWS) pour les diagrammes isométriques de la boîte à outils | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/isoicons) | +| [`mvvm`](https://github.com/go-widgets/mvvm) | v0.13.0 | [observables, commandes, binders, annulation]({{< relref "/state/mvvm.md" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/mvvm) | +| [`mvvmtk`](https://github.com/go-widgets/mvvmtk) | v0.14.1 | [des binders en un appel pour les widgets de la boîte à outils]({{< relref "/state/mvvm.md#mvvmtk-one-call-per-widget" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/mvvmtk) | +| [`data`](https://github.com/go-widgets/data) | v0.3.0 | [enregistrements typés, requêtes, stockage local ou distant]({{< relref "/state/data.md" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/data) | +| [`window`](https://github.com/go-widgets/window) | v0.86.1 | [une fenêtre native]({{< relref "/surfaces/native-window.md" >}}) sur sept back-ends | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/window) | +| [`application`](https://github.com/go-widgets/application) | v0.7.0 | [le cycle de vie de l'application]({{< relref "/surfaces/application.md" >}}) au-dessus d'une fenêtre | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/application) | +| [`tray`](https://github.com/go-widgets/tray) | v0.14.0 | [une icône de zone de notification et son menu]({{< relref "/surfaces/application.md#the-tray" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/tray) | +| [`webcanvas`](https://github.com/go-widgets/webcanvas) | v0.2.0 | [une scène dans un `<canvas>` de navigateur]({{< relref "/surfaces/browser.md" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/webcanvas) | +| [`tui`](https://github.com/go-widgets/tui) | v0.61.0 | [rendu en terminal et widgets natifs en cellules]({{< relref "/surfaces/terminal.md" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/tui) | +| [`android`](https://github.com/go-widgets/android) | v0.15.0 | [un APK peint par un processus sans CGO]({{< relref "/surfaces/android.md" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/android) | +| [`svg`](https://github.com/go-widgets/svg) | v0.6.0 | [un rendu en SVG ou en PNG]({{< relref "/surfaces/snapshots.md" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/svg) | +| [`mvvmlint`](https://github.com/go-widgets/mvvmlint) | v0.4.0 | [le contrôle MVVM]({{< relref "/guard-rails/mvvmlint.md" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/mvvmlint) | +| [`bricolint`](https://github.com/go-widgets/bricolint) | v0.4.0 | [le contrôle « pas d'interface dessinée à la main »]({{< relref "/guard-rails/bricolint.md" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/bricolint) | +| [`app-template`](https://github.com/go-widgets/app-template) | v0.4.0 | [le point de départ d'une application]({{< relref "/getting-started.md" >}}) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/app-template) | +| [`gallery`](https://github.com/go-widgets/gallery) | v0.6.0 | la [démonstration en direct](https://go-widgets.github.io/gallery/) de chaque famille de widgets | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/gallery) | +| [`desktop`](https://github.com/go-widgets/desktop) | v0.19.0 | un shell de bureau qui assemble go-freedesktop et go-widgets, en natif et dans wasmdesk | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/desktop) | + +## Ce que chaque module doit respecter {#what-every-module-is-held-to} + +- `CGO_ENABLED=0` : pas de cgo, et pas d'appel à un outil en ligne de commande à la place + d'une bibliothèque. +- Compilé de manière croisée pour amd64, arm64, riscv64, loong64, ppc64le et s390x — ce dernier + en gros-boutiste, ce qui garde chaque encodage binaire honnête — et, quand cela + s'applique, `js/wasm`. Douze des dix-neuf couvrent les six aujourd'hui, deux sont des + applications de navigateur, et les cinq autres sont listés dans [État]({{< relref "/status.md#what-ci-does-not-cover-yet" >}}). +- Une barrière de couverture des instructions dans la CI : 100 % dans dix-huit des dix-neuf. Quand un + fichier ne peut pas s'exécuter dans un test (une boucle de navigateur, une boucle d'exécution native), le module + indique quel fichier est laissé de côté et pourquoi. Voir [État]({{< relref "/status.md" >}}) + pour les exceptions. +- BSD-3-Clause. diff --git a/content/fr/rendering/_index.md b/content/fr/rendering/_index.md new file mode 100644 index 0000000..26b2554 --- /dev/null +++ b/content/fr/rendering/_index.md @@ -0,0 +1,29 @@ +--- +title: "Comment un widget atteint l'écran" +linkTitle: "Rendu" +weight: 10 +description: "Un widget dessine à travers un painter ; le painter décide si cela devient des pixels ou des cellules de terminal ; un back-end présente le résultat." +tags: [rendu, painter, toolkit] +--- + +Trois couches, et chacune ne connaît que celle du dessous : + +```text +toolkit widget tree Button, Table, Agenda, VBox ... (or a skin.Object) + │ Draw(p painter.Painter, theme *Theme) + ▼ +painter.Painter FillRect, StrokeRect, Text, PutPixel, ... + │ + ├─ PixelPainter → an RGBA []byte → window, browser canvas, PNG, Android + └─ CellPainter → a grid of cells → a terminal, as 24-bit ANSI +``` + +Un widget n'apprend jamais quel painter on lui a remis. Un back-end n'apprend jamais +quels widgets ont dessiné. C'est toute la raison pour laquelle un même arbre de widgets peut +s'afficher dans une fenêtre native, un navigateur et un terminal sans une ligne de code conditionnel. + +| | | +|---|---| +| [La jonction du painter]({{< relref "/rendering/painter.md" >}}) | les primitives, les capacités optionnelles, les deux painters | +| [La boîte à outils]({{< relref "/rendering/toolkit.md" >}}) | le jeu de widgets, les thèmes, les mises en page, le texte et l'accessibilité | +| [Skins]({{< relref "/rendering/skin.md" >}}) | décrire les parties et les états d'un widget par des données plutôt qu'en Go | diff --git a/content/fr/rendering/painter.md b/content/fr/rendering/painter.md new file mode 100644 index 0000000..a09d967 --- /dev/null +++ b/content/fr/rendering/painter.md @@ -0,0 +1,71 @@ +--- +title: "La jonction du painter" +linkTitle: "Painter" +weight: 10 +description: "Les primitives avec lesquelles chaque widget est dessiné, les capacités optionnelles qu'un painter peut ajouter, et les painters en pixels et en cellules." +tags: [rendu, painter] +--- + +[`go-widgets/painter`](https://github.com/go-widgets/painter) définit l'unique +interface à travers laquelle chaque widget dessine : + +```go +type Painter interface { + FillRect(r Rect, c RGBA) + StrokeRect(r Rect, c RGBA, lineW int) + FillRoundRect(r Rect, radius int, c RGBA) + StrokeRoundRect(r Rect, radius int, c RGBA, lineW int) + PutPixel(x, y int, c RGBA) + Text(x, y int, s string, ink RGBA) + Size() (w, h int) +} +``` + +L'ensemble est petit à dessein : chaque back-end doit l'implémenter en entier, et une +grille de terminal ne peut ni arrondir un coin ni faire varier l'épaisseur d'un trait. Le contrat +dit donc ce qui se passe quand c'est impossible — l'épaisseur de `StrokeRect` est une indication qu'une +grille de cellules ignore, et `FillRoundRect` s'y replie sur un remplissage carré. + +## Capacités optionnelles {#optional-capabilities} + +Ce que toutes les surfaces ne savent pas faire n'est pas dans l'interface de base. Un widget qui +en a besoin fait une assertion de type, et dessine quelque chose de raisonnable quand la réponse est non : + +| Interface | Méthodes | Sert à | +|---|---|---| +| `Clipper` | `PushClip`, `PopClip` | garder un enfant défilé dans les limites de son parent | +| `Translator` | `PushTranslate`, `PopTranslate` | dessiner un enfant dans ses propres coordonnées | +| `PathPainter` | `FillPath`, `StrokePath` | courbes, graphiques, formes anticrénelées | +| `ImagePainter` | `DrawImage` | icônes, vignettes, images | +| `MaskPainter` | `DrawMask` | glyphes et autres masques de couverture d'une seule couleur | +| `FacePainter` | `TextFace` | du texte dans une police et une taille données | + +```go +if c, ok := p.(painter.Clipper); ok { + c.PushClip(bounds) + defer c.PopClip() +} +``` + +## Les deux painters {#the-two-painters} + +| | Écrit dans | Présenté par | +|---|---|---| +| `NewPixelPainter(buf, w, h)` | un `[]byte` RGBA appartenant à l'appelant (`NewPixelPainterBGRA` pour les surfaces BGRA) | une fenêtre native, un canvas de navigateur, une surface Android, un PNG ou un SVG | +| `NewCellPainter(w, h)` | une grille de cellules (rune, premier plan, arrière-plan) avec un sérialiseur ANSI 24 bits | un terminal, via [`tui`]({{< relref "/surfaces/terminal.md" >}}) | + +Le tampon reste celui de l'appelant. Un `PixelPainter` ne fait que traduire les primitives +en écritures, si bien que le même `[]byte` peut être remis à un back-end de fenêtre, à un +`<canvas>` ou à un encodeur d'images sans copie intermédiaire. + +## Essayer {#trying-it} + +Les démos du dépôt lui-même dessinent les trois mêmes widgets avec les deux painters : + +```sh +go run ./cmd/wui-demo --out demo.png # pixels, written as a PNG +go run ./cmd/tui-demo --theme dark # cells, written as ANSI to stdout +``` + +et [go-widgets.github.io/painter](https://go-widgets.github.io/painter/) montre +le painter en pixels dans un navigateur. diff --git a/content/fr/rendering/skin.md b/content/fr/rendering/skin.md new file mode 100644 index 0000000..6d6c3c7 --- /dev/null +++ b/content/fr/rendering/skin.md @@ -0,0 +1,81 @@ +--- +title: "Skins : les parties et les états d'un widget sous forme de données" +linkTitle: "Skins" +weight: 30 +description: "go-widgets/skin décrit les parties d'un widget, son apparence par état et ses transitions pilotées par des signaux dans un fichier .skin.json, rendu par un seul moteur." +tags: [rendu, skin, thème] +--- + +Les widgets de la boîte à outils sont écrits en Go : les parties d'un `Button`, ses apparences +au survol, appuyé et désactivé, et son retour au clic vivent tous dans `button.go`. +C'est rapide et exact, et chaque changement visuel est un changement de code. + +[`go-widgets/skin`](https://github.com/go-widgets/skin) fait passer cette description +dans des données — l'idée d'Edje, d'Enlightenment. Un document `.skin.json` nomme les +**parties** d'un widget, leur aspect dans chaque **état**, et les **programmes** qui +les font passer d'un état à l'autre quand un signal arrive. Changer de skin, ou construire un nouveau +contrôle, ne demande alors aucune recompilation : le programme charge d'autres octets. + +```go +theme, err := skin.Load(jsonBytes) // parse and validate every collection +if err != nil { + return err +} +obj, _ := theme.New("button") +obj.Bind(skin.NewMVVMContext().Set("$.label", labelObs)) +obj.SetBounds(toolkit.Rect{W: 96, H: 28}) + +// each frame +obj.OnEvent(ev) // pointer events become signals, signals states +busy := obj.Tick(time.Now()) // advance the transitions +obj.Draw(p, theme.Palette()) +``` + +Un `skin.Object` est un `toolkit.Widget` ; il prend donc place dans une `VBox` à côté de +widgets écrits à la main. Les mises en page placent les widgets ; une skin agence ce qui est +à l'intérieur de l'un d'eux. + +## Le format {#the-format} + +```jsonc +{ + "collections": { + "button": { + "min": { "w": 96, "h": 28 }, + "parts": [ + { "name": "bg", "type": "rect", + "states": { + "default": { "color": "@surface", "border": "@border", "radius": 6 }, + "hover": { "color": "@surface_alt", "border": "@border", "radius": 6 }, + "pressed": { "color": "@accent", "border": "@border", "radius": 6 } } }, + { "name": "label", "type": "text", "text_from": "$.label", "align": [0.5, 0.5], + "states": { "default": { "ink": "@on_surface" } } } + ], + "programs": [ + { "on": "mouse,in", "target": ["bg", "label"], "to": "hover", "in": 0.12, "ease": "ease_out_cubic" }, + { "on": "mouse,click", "emit": "clicked" } + ] + } + } +} +``` + +- Une **partie** est un `rect`, un `text` ou une `image`, placée par deux coins relatifs + (`rel1`, `rel2`) par rapport à l'objet ou à une partie antérieure, avec des décalages en pixels et + relatifs à la police (`offset_em`), si bien qu'une partie texte suit la police. +- Un **état** fixe les couleurs, la bordure, le rayon et la visibilité, et peut déplacer ou + redimensionner la partie. +- Un **programme** réagit à un signal, fait passer des parties dans un état sur une durée donnée + avec une courbe d'accélération nommée, et peut émettre (`emit`) un signal vers l'extérieur — vers une + [`mvvm.Command`]({{< relref "/state/mvvm.md" >}}), par exemple. +- Les couleurs sont des `@tokens` résolus d'après le `Theme` de la boîte à outils (y compris chaque + entrée GTK `@define-color` sous la forme `@extra:<name>`), de l'hexadécimal, ou des tableaux RGBA. + +Le JSON est décodé par la bibliothèque standard : aucune dépendance tierce. + +## Éprouvé face aux widgets écrits à la main {#proven-against-the-hand-written-widgets} + +Le bouton, la case à cocher, l'interrupteur, la puce et la carte de la boîte à outils sont chacun réécrits +sous forme de collection de skin, et les tests vérifient que les deux rendus sont **identiques au pixel +près** dans chaque état. Un `Draw` avec skin est comparé au +`Draw` écrit à la main dans un benchmark. diff --git a/content/fr/rendering/toolkit.md b/content/fr/rendering/toolkit.md new file mode 100644 index 0000000..80dbcc9 --- /dev/null +++ b/content/fr/rendering/toolkit.md @@ -0,0 +1,74 @@ +--- +title: "La boîte à outils" +linkTitle: "Toolkit" +weight: 20 +description: "160 types de widgets dessinés à travers la jonction du painter : thèmes, mises en page, texte, saisie, accessibilité." +tags: [rendu, toolkit, widgets, thème, accessibilité] +--- + +[`go-widgets/toolkit`](https://github.com/go-widgets/toolkit) est le jeu de +widgets. Chaque widget implémente la même interface — `Bounds`, `SetBounds`, +`Draw(p painter.Painter, theme *Theme)`, `HitTest`, `OnEvent` — si bien qu'un hôte +traite de la même façon un bouton, un tableau et un calendrier d'événements. Bibliothèque standard uniquement, +`CGO_ENABLED=0`, 100 % de couverture des instructions. + +En v0.328.0, elle compte **160 types de widgets exportés** en environ **69 000 lignes** de +code de widgets. Ces deux nombres sont mesurés à cette étiquette, pas reportés : le +README de la boîte à outils donne les commandes, et il vaut mieux les relancer que +répéter les chiffres. + +## Les familles {#the-families} + +| Famille | Widgets | +|---|---| +| Action | `Button`, `ToggleButton`, `CheckButton`, `RadioButton` + `RadioGroup`, `Switch`, `SplitButton`, `IconButton`, `CycleButton`, `Chip`, `SegmentedBar` | +| Saisie | `Entry`, `TextView` (sélection, aperçu IME, plages de syntaxe, numéros de ligne), `SpinButton`, `Scale`, `RangeSlider`, `SearchEntry`, `TagField`, `ComboBox`, `FormField` avec `Validate`/`Rule` | +| Sélection | `ListBox`, `TreeView`, `DropDown` | +| Conteneurs | `Container` avec un `Layout` interchangeable (`FitLayout`, `BoxLayout`, `BorderLayout`, `CardLayout`, `FlowLayout`) ; `HBox`, `VBox`, `Grid`, `Frame`, `Dock`, `Border`, `Stack`, `Overlay`, `Paned`, `Expander`, `Accordion` ; un constructeur déclaratif `Node` | +| Navigation | `Menu`, `MenuBar` (avec `Alt`+lettre), `ContextMenu`, `Popover`, `CommandPalette`, `Dialog`, `MessageDialog`, `Wizard`, `Notebook`, `ViewSwitcher`, `Carousel` | +| Retour | `ProgressBar`, `ProgressCircle`, `LevelBar`, `Spinner`, `Tooltip`, `Notification`, `Toast`, `Banner`, `Alert`, `Badge`, `Skeleton` | +| Données | `Table` (édition de cellules, colonnes figées, lignes de groupe, agrégats, lignes dépliables), `TreeTable`, `PropertyGrid`, `Kanban`, `Gantt`, `Agenda` (semaine, mois, trimestre, année ; plusieurs calendriers) | +| Graphiques | `LineChart`, `BarChart`, `PieChart`, `AreaChart`, `ScatterChart`, `RadarChart`, `Gauge`, `Sparkline` | +| Composites | `FileChooser`, `ColorChooser`, `ColorPicker`, `FontChooser`, `Calendar`, `DatePicker`, `DateRangePicker`, `TimePicker`, `MarkdownView`, `MarkdownEditor`, `TerminalView` | +| Shell | `Window` avec décorations côté client, `HeaderBar`, `Toolbar`, `Statusbar`, `StatusIcon`, `StatusArea`, `Wallpaper`, `Thumbnail` | + +Tous, en direct : la [galerie](https://go-widgets.github.io/gallery/). + +## Thèmes {#themes} + +Une seule valeur `Theme` se propage en cascade dans tout l'arbre : changez une couleur à la +racine et chaque widget se repeint avec elle. `DefaultLight()` et `DefaultDark()` +sont intégrés, et `LoadGTKTheme(css)` lit le bloc `@define-color` de n'importe quel +thème GTK 3 ou libadwaita — Adwaita, WhiteSur, Solarized — dans un `Theme`. + +## Texte {#text} + +Par défaut, la police compilée est une police bitmap 5×7, pour que les tests au pixel gardent leur +géométrie. Un seul appel fait passer toute l'interface à un texte anticrénelé et mis en forme : + +```go +toolkit.UseOpenTypeText() // Atkinson Hyperlegible, bundled; js/wasm-safe +``` + +La police vient de [go-opentype/fonts](https://github.com/go-opentype/fonts) +et est rastérisée par [go-opentype](https://github.com/go-opentype/opentype), +en Go, sans bibliothèque de polices en C ni recherche de polices système. +`NewTrueTypeFont(ttf, px)` charge une autre police ; `NewFallbackFont` en enchaîne +plusieurs, par exemple pour ajouter une police CJK ou arabe ; le champ `Font` propre à un widget +remplace la police globale pour ce seul widget. + +## Saisie {#input} + +Souris, clavier, composition IME, tactile (`EventTouchStart`/`Move`/`End`, avec +`GestureRecognizer` pour les tapes, les appuis longs et les balayages), glisser-déposer +(`DragSource`, `DropTarget`). La sélection de plusieurs lignes dans `ListBox`, `Table` et +`TreeView` fonctionne à la souris comme au clavier, et respecte ⌘ aussi bien +que Ctrl, car sous macOS Ctrl-clic est le clic secondaire. + +## Accessibilité {#accessibility} + +Une boîte à outils qui peint ses propres pixels ne donne rien à lire à un lecteur d'écran +à moins de dire ce qu'elle a dessiné. Les widgets implémentent `Accessible` — un rôle, un nom, +une valeur — et `CollectA11y` les rassemble pour que l'hôte les publie : AT-SPI sous +Linux via [`window`]({{< relref "/surfaces/native-window.md" >}}), et une +hiérarchie de vues virtuelle sur [Android]({{< relref "/surfaces/android.md#accessibility" >}}). diff --git a/content/fr/state/_index.md b/content/fr/state/_index.md new file mode 100644 index 0000000..12ef18b --- /dev/null +++ b/content/fr/state/_index.md @@ -0,0 +1,23 @@ +--- +title: "L'état vit dans un view-model" +linkTitle: "Gestion de l'état" +weight: 20 +description: "Pourquoi l'état d'une application est conservé dans des observables liés aux widgets, plutôt qu'écrit dans les champs des widgets." +tags: [état, mvvm] +--- + +Les champs d'un widget — le texte d'un champ de saisie, les éléments d'une liste, les lignes d'un tableau — sont sa +**vue** de l'état, pas l'état. Une application qui les affecte depuis +l'endroit où elle se trouve disperse son état entre les sites d'appel, et deux de +ces sites d'appel finissent par ne plus s'accorder sur ce qui est à l'écran. + +L'état vit donc dans un **view-model** : de simples valeurs Go enveloppées dans des observables, +sans aucun widget en vue, testables sans fenêtre. La vue lie chaque widget +au view-model une fois, et à partir de là c'est la liaison — pas l'application — qui +les garde synchronisés, dans les deux sens. + +| | | +|---|---| +| [MVVM]({{< relref "/state/mvvm.md" >}}) | `Observable`, `Command`, `ObservableList` ; des binders pour les widgets de la boîte à outils et du terminal ; annuler et rétablir | +| [La colonne vertébrale des données]({{< relref "/state/data.md" >}}) | des enregistrements typés avec validation, des requêtes, et un stockage qui exécute le même pipeline en local ou via gRPC | +| [`mvvmlint`]({{< relref "/guard-rails/mvvmlint.md" >}}) | le contrôle de CI qui fait échouer une pull request affectant à la main un champ d'état d'un widget | diff --git a/content/fr/state/data.md b/content/fr/state/data.md new file mode 100644 index 0000000..e38b469 --- /dev/null +++ b/content/fr/state/data.md @@ -0,0 +1,45 @@ +--- +title: "La colonne vertébrale des données" +linkTitle: "Données" +weight: 20 +description: "go-widgets/data : des enregistrements typés avec validation, un moteur de requêtes pur, et un stockage qui exécute le même pipeline dans le processus ou via gRPC, en natif comme en wasm." +tags: [état, données, grpc] +--- + +[`go-widgets/data`](https://github.com/go-widgets/data) est sans interface : il +importe la bibliothèque standard et [`mvvm`]({{< relref "/state/mvvm.md" >}}), +et aucun widget. + +| Élément | Ce que c'est | +|---|---| +| `Value`, `Kind` | un scalaire typé comparable : chaîne, entier, flottant, booléen | +| `Record`, `Schema`, `Field`, `Rule` | une ligne typée et sa validation | +| `Query` → `Apply` → `View` | le moteur de requêtes : filtrer, trier, grouper, paginer, agréger | +| `Proxy` | la jonction avec le back-end : `List`, `Query`, `Mutate` | +| `MemoryProxy` | le back-end dans le processus | +| `grpcproxy.Server`, `grpcproxy.Client` | le même contrat via gRPC, transporté sur une WebSocket | +| `Store[R]` | une collection typée, publiée sous forme d'`mvvm.ObservableList` | + +## La même réponse, en local ou à distance {#the-same-answer-local-or-remote} + +`Store` parle à un `Proxy` sans jamais savoir lequel. `Apply` est une fonction +pure des enregistrements et de la requête, si bien que le client et le serveur exécutent le +même code sur les mêmes lignes — et un `MemoryProxy` et un `grpcproxy.Client` +renvoient des vues identiques à l'octet près. Le test de conformité vérifie exactement cela : une +batterie de requêtes de tri, de filtre, de groupement, de pagination et d'agrégation à travers les deux +proxys et à travers le moteur, chaque résultat mis sous forme canonique, les trois +comparés octet par octet. + +Le transport WebSocket se compile pour `js/wasm`, si bien qu'une application de navigateur +atteint le même service par le même client. + +```go +store := data.NewStore(mem, codec) // or a grpcproxy.Client: nothing else changes +store.SetQuery(data.Query{Sorts: []data.Sort{{Field: "salary", Desc: true}}, Limit: 20}) +store.Load(ctx) +items := store.Items() // bind this to a view +``` + +`tkbind.BindTable` place un stockage derrière une `Table` de la boîte à outils : un clic sur l'en-tête +trie, un regroupement groupe, et une modification de cellule validée devient une mutation via +le proxy — le même aller-retour, en local ou à distance. diff --git a/content/fr/state/mvvm.md b/content/fr/state/mvvm.md new file mode 100644 index 0000000..d956d6c --- /dev/null +++ b/content/fr/state/mvvm.md @@ -0,0 +1,95 @@ +--- +title: "MVVM : observables, commandes, binders" +linkTitle: "MVVM" +weight: 10 +description: "go-widgets/mvvm, ses binders pour la boîte à outils et le terminal, les aides en un appel de mvvmtk, et une pile d'annulation." +tags: [état, mvvm, mvvmtk, annulation] +--- + +[`go-widgets/mvvm`](https://github.com/go-widgets/mvvm) repose uniquement sur les génériques, sans +réflexion. Son paquet central n'importe aucun paquet de widgets, si bien qu'un même +view-model pilote aussi bien la [boîte à outils]({{< relref "/rendering/toolkit.md" >}}) en pixels +que les [widgets de terminal]({{< relref "/surfaces/terminal.md" >}}). + +| Primitive | Rôle | +|---|---| +| `Observable[T]` | une propriété : `Get`, `Set`, `Subscribe`. Affecter une valeur égale ne fait rien. | +| `Command` | une action, avec `CanExecute` et `RaiseCanExecuteChanged` | +| `ObservableList[T]` | une collection qui signale chaque insertion, suppression, remplacement, déplacement et réinitialisation | + +## Un formulaire {#a-form} + +```go +type FormVM struct { + Name *mvvm.Observable[string] + Names *mvvm.ObservableList[string] + Save *mvvm.Command +} + +vm := &FormVM{Name: mvvm.NewObservable(""), Names: mvvm.NewObservableList[string]()} +vm.Save = mvvm.NewCommand( + func() { vm.Names.Append(vm.Name.Get()); vm.Name.Set("") }, + func() bool { return vm.Name.Get() != "" }, // CanExecute +) +mvvm.BindCanExecute(vm.Save, vm.Name) // Save greys out while the name is empty +``` + +## Liaison {#binding} + +Les binders génériques atteignent un widget par un **pointeur vers son champ de valeur et +un pointeur vers son emplacement de callback** : `BindField` (bidirectionnel), `OneWay`, +`BindCommand`, `BindList`. Un widget déclenche son callback depuis son gestionnaire d'événements +et laisse silencieuse une écriture directe dans le champ, et `Observable.Set` ignore une valeur +égale — si bien qu'une liaison bidirectionnelle peut renvoyer l'écho sans récursion. + +La boîte à outils fait évoluer ses widgets pour qu'ils exposent leur état **sous forme** d'un +observable (`Label.Text()` renvoie un `*mvvm.Observable[string]`). Il n'y a +alors plus de champ à initialiser ni de callback à composer, seulement deux propriétés qui +doivent s'accorder : `BindTwoWay(src, dst, invalidate)`. + +Les widgets dont le callback prend plusieurs arguments reçoivent un petit adaptateur nommé dans un +paquet propre à chaque back-end, les seuls paquets qui importent un back-end : + +| Paquet | Adaptateurs | +|---|---| +| `mvvm/tkbind` | `BindRange` (le `RangeSlider` à deux poignées), `BindContainer` et `BindCardActive` (les enfants d'un conteneur et une mise en page en cartes, depuis une liste ou un index), `BindStore` et `BindTable` (une `Table` au-dessus d'un [stockage de données]({{< relref "/state/data.md" >}}) : tri, regroupement et modifications en ligne validées) | +| `mvvm/tuibind` | `BindDropdown`, `BindTableSelection` | + +## mvvmtk : un appel par widget {#mvvmtk-one-call-per-widget} + +[`go-widgets/mvvmtk`](https://github.com/go-widgets/mvvmtk) renseigne les vrais noms de champ et de +callback de chaque widget de la boîte à outils, si bien que la vue ne les nomme jamais : + +```go +unbind := mvvmtk.BindText(search, vm.Query, win.Invalidate) // search.Text ⇄ vm.Query +defer unbind() +``` + +| Aide | Sens | +|---|---| +| `BindText` (`SearchEntry`), `BindEntryText`, `BindChecked`, `BindSelectedIndex`, `BindSpin`, `BindListSelection`, `BindViewSwitcher` | bidirectionnel | +| `BindLabel`, `BindProgress` | view-model → widget | +| `BindListItems`, `BindDropDownOptions`, `BindViews`, `BindTree` | liste → widget | +| `BindCommand` | le clic d'un bouton, son état `Disabled` et son grisage suivent la commande | + +Chaque aide renvoie un `unbind` qui détache la liaison et restaure le +callback qui était là auparavant. Un bouton lié à une commande qui ne peut pas s'exécuter +n'accepte ni clic, ni Entrée ou Espace, ni focus clavier. + +## Annuler et rétablir {#undo-and-redo} + +`mvvm/undo` est une pile de commandes construite sur les seules primitives centrales ; elle +fonctionne donc sous n'importe quelle vue : + +```go +s := undo.New() // unlimited, coalescing on; undo.WithLimit(n), undo.WithCoalescing(false) +s.Push(undo.NewCommand("Write hello", do, undoIt)) +s.Undo() +s.Redo() + +mvvm.BindCommand(s.UndoCommand(), &undoBtn.OnClick, setEnabled) +mvvm.OneWay(s.UndoTextBinding(), &undoBtn.Text, repaint) // "Undo Write hello" +``` + +Une suite de frappes se fusionne en une seule étape, et un ajout après une annulation abandonne +la suite des actions à rétablir. diff --git a/content/fr/status.md b/content/fr/status.md new file mode 100644 index 0000000..b30d18c --- /dev/null +++ b/content/fr/status.md @@ -0,0 +1,64 @@ +--- +title: "État" +weight: 60 +description: "Ce qui a été prouvé sur un vrai écran, un vrai appareil ou un vrai terminal ; ce qui est seulement compilé ; et ce que la CI ne couvre pas encore." +tags: [état] +--- + +Ces pages ont été vérifiées par rapport aux modules aux étiquettes listées dans +[Modules]({{< relref "/modules.md" >}}). Quand le README d'un module et son code +divergent, les pages suivent le code. + +## Prouvé en conditions réelles {#proven-live} + +Un codec testé dans le processus prouve le codec. Voici les endroits où l'on a interrogé +la plateforme elle-même : + +| Quoi | Où cela a été prouvé | +|---|---| +| Fenêtre X11 | CI, sous Xvfb : un motif connu présenté, capturé et échantillonné ; un clic et une touche synthétisés avec `xdotool` et le `toolkit.Event` distribué vérifié ; `Show`/`Hide`/`Raise` relus auprès du serveur | +| Fenêtre Cocoa | une voie de CI macOS : une fenêtre ouverte et rendue, des pixels échantillonnés, un clic et une touche synthétisés ; `Show`/`Hide`/`Raise` demandés à une vraie `NSWindow` depuis une autre goroutine et relus auprès d'AppKit | +| Fenêtre Win32 | une VM Windows 11 arm64 : une vraie fenêtre rendant une `VBox`, un `Label` et un `Button`, trois clics injectés faisant passer son compteur de 0 à 3 via la procédure de fenêtre | +| Fenêtre Wayland | face à un faux compositeur qui impose les règles de map et d'unmap de xdg-shell | +| Fenêtre wasmbox | un Chromium headless pilotant le vrai compositeur wasmdesk : les pixels composités relus, un vrai clic acheminé jusqu'au widget | +| Android | un appareil Android 15 arm64 : copies limitées aux zones endommagées mesurées, mémoire sale du memfd mesurée, arbre d'accessibilité évalué sur l'appareil | +| Zone de notification macOS | une vraie session macOS : l'icône quitte la barre des menus quand le programme s'arrête, revient quand il démarre, et ouvre son menu | +| Zone de notification Linux | CI face à un vrai bus de session : le nom est réclamé, le menu répond à `GetLayout`, `Quit` le retire | +| Zone de notification Windows | l'icône et son menu sur une VM Windows 11 | +| Démos en terminal | un vrai pty en CI : octets de touches envoyés, image rendue vérifiée | + +## Compilé, non exécuté {#compiled-not-run} + +- `Show`/`Hide`/`Raise` sous Win32 : la table de décision est testée unitairement, les appels + sur le thread de la fenêtre sont seulement compilés. +- L'`Attach` de la zone de notification Windows. +- La boucle DOM de `webcanvas` (`run_js.go`) : derrière une étiquette de compilation `js && wasm`, elle + est donc hors de la couverture mesurée ; les applications qui l'utilisent l'exercent. +- Le `Run` natif d'`application` : l'ouverture d'une vraie fenêtre est exclue de sa + barrière de couverture ; le contrat, la traduction des événements et le compteur de disponibilité + sont couverts. + +## Non fait {#not-done} + +- Les zones de notification Windows et Linux ignorent `MenuItem.Icon` : une ligne avec une icône + se dessine sans elle. +- Le back-end Wayland de `window` ne sait pas faire `Raise` : xdg-shell n'a pas de telle requête, + et xdg-activation exige un jeton issu d'une action de l'utilisateur lui-même. +- Android : `android/arm64` uniquement, car c'est la seule cible Android que Go + sait lier sans cgo. +- `window.Screens` renvoie `ErrScreensUnsupported` sur `js/wasm`. + +## Ce que la CI ne couvre pas encore {#what-ci-does-not-cover-yet} + +Mesuré à partir des workflows de chaque dépôt aux étiquettes ci-dessus : + +| Module | Manque | +|---|---| +| `mvvmtk` | pas de barrière de couverture ; compilation croisée pour amd64 et arm64 seulement, sur six systèmes d'exploitation | +| `application` | compilation croisée pour Linux, macOS et Windows sur amd64 seulement | +| `tray` | compilation croisée pour quatre paires système/architecture | +| `mvvmlint` | pas de compilation croisée | +| `bricolint` | son étape « 6 arches » en compile cinq : loong64 manque | + +`app-template` et `gallery` sont des applications de navigateur et se compilent pour `js/wasm`, +qui est leur seule cible. diff --git a/content/fr/surfaces/_index.md b/content/fr/surfaces/_index.md new file mode 100644 index 0000000..7843bab --- /dev/null +++ b/content/fr/surfaces/_index.md @@ -0,0 +1,31 @@ +--- +title: "Où elle peint" +linkTitle: "Surfaces" +weight: 30 +description: "Toutes les surfaces sur lesquelles un arbre go-widgets peut s'afficher, et ce qui décide entre elles." +tags: [surfaces, back-ends] +--- + +| Surface | Module | Painter | +|---|---|---| +| [Une fenêtre native]({{< relref "/surfaces/native-window.md" >}}) sur X11, Wayland, macOS, Windows | `window` | pixels | +| [Une application]({{< relref "/surfaces/application.md" >}}) avec une icône de zone de notification et l'apparence du système | `application`, `tray` | pixels | +| [Un onglet de navigateur]({{< relref "/surfaces/browser.md" >}}) : un simple `<canvas>` | `webcanvas` | pixels | +| [Un bureau dans le navigateur]({{< relref "/surfaces/browser.md#a-window-on-the-wasmdesk-desktop" >}}) : une fenêtre du compositeur wasmdesk | `window` (wasmbox) | pixels | +| [Un terminal]({{< relref "/surfaces/terminal.md" >}}) | `tui` | cellules | +| [Un APK Android]({{< relref "/surfaces/android.md" >}}) | `android`, `window` | pixels | +| [Une image]({{< relref "/surfaces/snapshots.md" >}}) : SVG ou PNG | `svg` | pixels | + +L'arbre de widgets ne change pas de l'une à l'autre. Ce qui change, c'est qui possède le +tampon et qui fournit les entrées — un serveur d'affichage, un navigateur, un terminal, un +hôte Java — et chaque back-end transforme ces entrées en un même `toolkit.Event`. + +## Choisie selon ce qui est présent {#chosen-by-what-is-there} + +`window.Open` ne prend aucun argument de back-end. Il examine l'environnement — la +plateforme pour laquelle il a été compilé, `$WAYLAND_DISPLAY`, `$DISPLAY`, le socket qu'exporte un +hôte Android — et choisit celui qui peut fonctionner. Un binaire compilé pour +Linux ouvre une fenêtre Wayland dans une session Wayland et une fenêtre X11 sous +X ; le même binaire Android contacte son hôte dans un APK et ouvre une fenêtre +Linux ordinaire sous Termux. Voir +[comment `Open` choisit]({{< relref "/surfaces/native-window.md#how-open-chooses" >}}). diff --git a/content/fr/surfaces/android.md b/content/fr/surfaces/android.md new file mode 100644 index 0000000..4442b06 --- /dev/null +++ b/content/fr/surfaces/android.md @@ -0,0 +1,64 @@ +--- +title: "Android" +linkTitle: "Android" +weight: 50 +description: "go-widgets/android : un APK installable dont toute l'interface est mise en page et peinte par un processus Go CGO_ENABLED=0." +tags: [surfaces, android] +--- + +Android est la seule plateforme où un processus sans CGO ne peut pas posséder de fenêtre : tout +chemin vers une surface dessinable passe par JNI, et JNI exige cgo. Il n'existe pas +de protocole réseau à parler à la place, comme en ont X11 et Wayland. + +[`go-widgets/android`](https://github.com/go-widgets/android) se compose donc de deux +processus : + +| | | +|---|---| +| **Hôte Java** (`host/`) | possède l'`Activity`, la `SurfaceView`, le tactile, les touches et le cycle de vie. Copie des pixels ; ne sait rien des widgets. | +| **Application Go** (`cmd/gwapp`) | un exécutable `CGO_ENABLED=0 GOOS=android` ordinaire. Possède la mise en page, les widgets, le thème, le test de position et le focus, inchangés par rapport à tous les autres back-ends. | + +L'hôte Java tient la place du serveur X sous Linux. Les pixels transitent par +un **memfd** que l'application crée et remet à l'hôte par le socket ; +le côté Go écrit du RGBA_8888, qui est octet pour octet l'ARGB_8888 d'Android, si bien que +la copie se fait sans conversion, et seulement pour le rectangle endommagé. Les entrées, +les marges (insets), le texte IME et l'arbre d'accessibilité passent par un protocole tramé sur un +`LocalSocket` abstrait. + +```go +c, err := android.Dial("my app", nil) // nil theme: toolkit.DefaultDark() +if errors.Is(err, android.ErrUnsupported) { + return nil // not under a host +} +defer c.Close() +return c.Run(myWidgetTree()) +``` + +`Client` satisfait le `Backend` de `window`, si bien qu'une application passe d'Android +aux back-ends de bureau sans changer une ligne au-dessus de la fenêtre — +et [`window.Open`]({{< relref "/surfaces/native-window.md#how-open-chooses" >}}) +le choisit de lui-même quand le socket de l'hôte est présent. + +## arm64 uniquement {#arm64-only} + +`android/arm64` est la seule cible Android que Go sait lier sans cgo ; `arm`, +`amd64` et `386` exigent une édition de liens externe. La CI vérifie les deux moitiés — celle +qui se compile et les trois qui ne se compilent pas — si bien que le jour où Go lèvera cette restriction, +la compilation le dira. + +## Accessibilité {#accessibility} + +Un lecteur d'écran verrait la `SurfaceView` comme un unique rectangle opaque. L'hôte +lui donne à la place une hiérarchie de vues virtuelle : un nœud par widget accessible, +avec la classe `android.widget.*` d'après laquelle Android choisit ses annonces, le +texte, les limites, et une action d'activation. L'arbre est **tiré** quand +quelque chose le lit, jamais poussé, si bien qu'une application à laquelle aucun service d'accessibilité +n'est attaché n'en construit jamais. Une activation revient sous forme d'un clic ordinaire au +centre de l'élément, par le même code que celui d'un toucher. + +## Tactile et animation {#touch-and-animation} + +Chaque échantillon tactile atteint l'arbre sous forme d'un événement tactile, puis d'un événement souris : le +premier pour les widgets qui comprennent les gestes, le second pour les nombreux qui n'écoutent que les +clics. Les widgets animés avancent sur une boucle d'images qui démarre quand quelque chose +commence à s'animer et s'arrête quand plus rien ne s'anime. diff --git a/content/fr/surfaces/application.md b/content/fr/surfaces/application.md new file mode 100644 index 0000000..77fab23 --- /dev/null +++ b/content/fr/surfaces/application.md @@ -0,0 +1,82 @@ +--- +title: "Une application et son icône de zone de notification" +linkTitle: "Application et zone de notification" +weight: 20 +description: "go-widgets/application gère le cycle de vie au-dessus d'une fenêtre ; go-widgets/tray place une icône et un menu dans la barre des menus ou la zone de notification." +tags: [surfaces, application, zone de notification] +--- + +## L'application {#the-application} + +[`go-widgets/application`](https://github.com/go-widgets/application) est la +couche au-dessus de [`window`]({{< relref "/surfaces/native-window.md" >}}) : elle ouvre +la fenêtre, pilote sa boucle, attache une icône de zone de notification une fois la première image affichée, +transmet à l'application l'apparence du système en direct (sombre ou claire, couleur d'accent, police système), +publie l'arbre d'accessibilité, et appelle `onReady` +une fois la première image à l'écran. + +```go +spec := application.Spec{ + Name: "News Reader", + Identifier: "com.example.reader", + Version: "1.0.0", + Tray: func() *tray.Menu { + return tray.NewMenu().Add( + tray.Item("Refresh", refresh), + tray.Item("Quit", quit), + ) + }, +} +cfg := application.Config{Title: "News Reader", Width: 1200, Height: 800} +err := application.Run(spec, cfg, handler, func() { log.Println("on screen") }) +``` + +`handler` implémente `Handler` — `Frame`, `Resize`, les événements de souris, de défilement et de +clavier — et peut implémenter n'importe laquelle des capacités optionnelles : apparence, +raccourcis, clics et touches avec modificateurs, clic secondaire, menus contextuels, +presse-papiers, accessibilité, contrôles natifs. La boucle n'enveloppe jamais le handler ; +elle fait une assertion de type pour chaque capacité et honore celles qu'elle trouve. Un test parcourt +le paquet à la recherche de ces assertions et échoue si la documentation du paquet ne +les nomme pas toutes. + +Une icône de zone de notification qui ne peut pas être attachée est une erreur de `Run`, pas un silence : +`errors.Is(err, tray.ErrNoBackend)` dit pourquoi. Une application qui veut sa +fenêtre quoi qu'il arrive à l'icône laisse `Spec.Tray` à nil et en lance une +elle-même. + +`Run` ferme sa fenêtre quand la boucle se termine, si bien que l'« Open » d'une application +de zone de notification peut rappeler `Run` pour une nouvelle fenêtre. Pour un hôte qui n'est pas une +fenêtre — un shell de bureau, un onglet de navigateur — `Bind` et `BindScaled` renvoient une +`*toolkit.Surface` branchée sur le même handler. + +## La zone de notification {#the-tray} + +Une icône de zone de notification relève de l'intégration au système, pas d'un widget peint ; +[`go-widgets/tray`](https://github.com/go-widgets/tray) pilote donc l'API propre à +chaque plateforme, toujours avec `CGO_ENABLED=0` : + +| Plateforme | API | Par | +|---|---|---| +| macOS | `NSStatusItem`, `NSMenu` | purego et le runtime Objective-C | +| Windows | `Shell_NotifyIcon`, `TrackPopupMenu` | `golang.org/x/sys/windows` | +| Linux | `StatusNotifierItem`, `com.canonical.dbusmenu` | DBus, en Go | + +```go +menu := tray.NewMenu().Add( + tray.Item("Open", open), + tray.Checkbox("Notifications", true, setNotify), + tray.SubMenu("Recent", tray.NewMenu().Add(tray.Item("file.txt", nil))), + tray.Separator(), + tray.Item("Quit", func() { t.Quit() }), +) +t := tray.New(iconPNG).SetTooltip("My App").SetMenu(menu) +t.Run() // blocks on the platform's loop until Quit +``` + +Un programme qui fait déjà tourner la boucle d'une fenêtre ne peut pas céder son thread principal à `Run` : +`Attach` affiche la même icône et rend la main, et `Quit` la retire. + +Les back-ends natifs sont activés par défaut ; il n'y a pas d'étiquette de compilation à retenir. +Sur une plateforme qui n'en a aucun, `Run` renvoie `ErrNoBackend` — toute la différence +entre « il n'y a pas de zone de notification ici » et une icône qui, en silence, ne fait rien. Un test +ou un service headless passe `WithBackend(tray.NewHeadless())`. diff --git a/content/fr/surfaces/browser.md b/content/fr/surfaces/browser.md new file mode 100644 index 0000000..ed30666 --- /dev/null +++ b/content/fr/surfaces/browser.md @@ -0,0 +1,54 @@ +--- +title: "Un onglet de navigateur" +linkTitle: "Navigateur" +weight: 30 +description: "Exécuter un arbre de widgets dans un simple <canvas> avec go-widgets/webcanvas, ou comme fenêtre du compositeur wasmdesk." +tags: [surfaces, wasm, navigateur, webcanvas, wasmdesk] +--- + +La boîte à outils est en Go pur, sans dépendance au DOM ; `GOOS=js GOARCH=wasm` la compile +donc comme n'importe quelle autre cible. Deux façons de mettre le résultat sous les yeux de quelqu'un : + +## Un simple canvas {#a-plain-canvas} + +[`go-widgets/webcanvas`](https://github.com/go-widgets/webcanvas) copie un framebuffer +RGBA dans un `<canvas>` et renvoie les événements DOM de pointeur et de clavier +vers la scène. Pas de compositeur, pas de `SharedArrayBuffer`, pas d'en-têtes d'isolation +cross-origin : n'importe quel hébergement statique le sert. + +```go +//go:build js && wasm + +package main + +import "github.com/go-widgets/webcanvas" + +func main() { webcanvas.Run("screen", myScene()) } +``` + +La scène implémente `App` : `Size`, `Draw(buf []byte)`, et une méthode par +type d'événement, chacune indiquant si quelque chose a changé, pour qu'une image inchangée +ne soit pas repeinte. Elle peut aussi implémenter `Ticker`, `Animator`, `Resizer` ou +`Scroller`. + +La [galerie](https://go-widgets.github.io/gallery/) et +[`app-template`]({{< relref "/getting-started.md" >}}) fonctionnent ainsi. + +## Une fenêtre sur le bureau wasmdesk {#a-window-on-the-wasmdesk-desktop} + +Sur `js/wasm`, [`window.Open`]({{< relref "/surfaces/native-window.md" >}}) +renvoie un client du compositeur de navigateur [wasmdesk/wasmbox](https://github.com/wasmdesk/wasmbox). +Il alloue sa surface dans un `SharedArrayBuffer`, dit +`hello` sur son `MessagePort`, attend `welcome`, peint dans le tampon partagé +et poste `commit` — la surface entière, ou seulement les rectangles endommagés. +Les messages `input` du compositeur deviennent des `toolkit.Event`, comme sous X11. + +Une application écrite pour `window` s'exécute donc sans modification comme fenêtre native +et comme fenêtre d'un bureau dans un onglet de navigateur. La +[démo du shell de bureau](https://github.com/go-widgets/desktop) fait exactement +cela, à partir d'une seule source. + +Le codec du protocole est couvert à 100 % sur chaque plateforme. La preuve en conditions réelles fait tourner +un Chromium headless face au vrai compositeur wasmdesk : elle lance le client, +lit les pixels composités par le compositeur lui-même, clique, et vérifie que le +clic a atteint le widget. diff --git a/content/fr/surfaces/native-window.md b/content/fr/surfaces/native-window.md new file mode 100644 index 0000000..bb54228 --- /dev/null +++ b/content/fr/surfaces/native-window.md @@ -0,0 +1,89 @@ +--- +title: "Une fenêtre native" +linkTitle: "Fenêtre native" +weight: 10 +description: "go-widgets/window : une seule API Open/Run sur X11, Wayland, Cocoa, Win32, GTK4, Android et wasmbox, toutes avec CGO_ENABLED=0." +tags: [surfaces, window, x11, wayland, macos, windows] +--- + +[`go-widgets/window`](https://github.com/go-widgets/window) ouvre une vraie fenêtre +et y fait tourner un arbre de widgets : mise en page, dessin, présentation, traduction des entrées en +`toolkit.Event`, et ainsi de suite jusqu'à la fermeture de la fenêtre. + +```go +w, err := window.Open(window.Config{Title: "Demo"}) +if err != nil { + return err +} +defer w.Close() +return w.Run(root) // root is any toolkit.Widget +``` + +`Config` prend aussi la taille initiale en **points logiques** (zéro demande une +taille lisible par défaut), l'instance et la classe `WM_CLASS`, un `Theme`, et un +`RenderScale`. + +## Comment Open choisit {#how-open-chooses} + +| Plateforme et environnement | Back-end | Comment il atteint l'écran | +|---|---|---| +| Linux, `$GO_WIDGETS_GTK` défini | GTK4 | GTK possède la fenêtre ; le framebuffer est une `GtkPicture`, et les contrôles natifs sont de vrais widgets GTK posés au-dessus. Exige le runtime libgtk-4. | +| Linux, `$WAYLAND_DISPLAY` défini | Wayland | xdg-shell sur le socket unix du compositeur | +| Linux, sinon `$DISPLAY` | X11 | le protocole X11 de base sur le socket unix, avec MIT-SHM | +| macOS | Cocoa | `NSWindow` et `NSView` via [go-macos/objc](https://github.com/go-macos/objc) (purego) | +| Windows | Win32 | un `HWND` de premier niveau via des appels système à user32 et gdi32, `StretchDIBits` | +| Android, `$GW_ANDROID_SOCKET` défini | hôte Android | un protocole tramé vers [l'hôte Java]({{< relref "/surfaces/android.md" >}}), les pixels dans un memfd partagé | +| Android, sinon | Wayland ou X11 | un shell sous Termux a toujours un serveur d'affichage à contacter | +| `js/wasm` | wasmbox | un client du [compositeur wasmdesk]({{< relref "/surfaces/browser.md#a-window-on-the-wasmdesk-desktop" >}}) | +| tout le reste | — | `window.ErrUnsupported`, pour qu'une compilation croisée compile quand même | + +Chaque back-end est `CGO_ENABLED=0`. Celui de X11 est le protocole de base écrit +de zéro sur le socket — pas de Xlib, pas de XCB — avec les deux ordres d'octets, le +cookie Xauthority, la correspondance des keysyms, `PutImage` découpé en tuiles sous la taille de +requête du serveur, et la voie rapide MIT-SHM, le segment partagé étant transmis par +`SCM_RIGHTS`. Windows est atteint par les DLL du processus lui-même avec +`syscall.NewLazyDLL` et une procédure de fenêtre `syscall.NewCallback` ; macOS +par le runtime Objective-C avec purego. + +La partie indépendante de la plateforme de chaque back-end — correspondance des événements, calculs de +coordonnées, empaquetage des pixels, rectangles endommagés — vit dans un codec couvert à 100 %, +sur chaque système d'exploitation. La fine couche de liaison avec la plateforme est éprouvée en conditions réelles : voir +[État]({{< relref "/status.md" >}}). + +## Seulement ce qui a changé {#only-what-changed} + +Une racine qui implémente `DamageRenderer` (comme le fait `toolkit/scene.HostRoot`) +signale les rectangles qu'elle a repeints, et les back-ends Cocoa, Win32 et wasmbox +ne présentent que ceux-là. + +## HiDPI {#hidpi} + +Par défaut, un pixel du framebuffer correspond à un point logique : l'interface est mise en page +et peinte à une taille lisible, et le compositeur la suréchantillonne. Sous Windows, +la fenêtre déclare une prise en charge du DPI par moniteur et le système met à l'échelle l'image +logique vers la zone client physique. `RenderScale` demande un framebuffer à +la résolution réelle de l'écran, ce qui n'est correct que pour une racine qui se met en page +en pixels de l'appareil. + +## Écrans, affichage et masquage {#screens-and-showing-and-hiding} + +`window.Screens()` liste les écrans connectés, le principal en premier, en points +logiques, panneaux du bureau exclus, et peut être appelé avant `Open`. +Le nom d'un écran est celui de la dalle elle-même (`"DELL U2720Q"`, d'après son EDID), avec repli +sur le connecteur (`"HDMI-1"`) quand elle n'en publie aucun — et aussi quand deux +dalles connectées publient le même, car un nom qui ne permet pas de les distinguer +n'est pas un nom. + +`window.Show`, `Hide` et `Raise` retirent de l'écran une fenêtre ouverte sans +la fermer, puis la ramènent — ce dont a besoin une [application de zone de notification]({{< relref "/surfaces/application.md#the-tray" >}}). + +| Back-end | Show | Hide | Raise | +|---|---|---|---| +| X11 | `MapWindow` | retrait ICCCM, pour qu'elle quitte la barre des tâches | map, raise, `_NET_ACTIVE_WINDOW` | +| Wayland | nouveau commit | tampon nul et commit | `ErrNotSupported` : xdg-shell n'a pas de telle requête | +| macOS | `orderFront:` | `orderOut:` | activer l'application, `makeKeyAndOrderFront:` | +| Windows | `SW_SHOWNA` | `SW_HIDE` | `SW_RESTORE` et `SetForegroundWindow` | +| GTK, Android, wasmbox | `ErrNotSupported` | `ErrNotSupported` | `ErrNotSupported` | + +Le focus, c'est à la plateforme de l'accorder : avec la prévention du vol de focus, `Raise` +peut seulement marquer la fenêtre comme demandant l'attention, sans pouvoir le savoir. diff --git a/content/fr/surfaces/snapshots.md b/content/fr/surfaces/snapshots.md new file mode 100644 index 0000000..804a2eb --- /dev/null +++ b/content/fr/surfaces/snapshots.md @@ -0,0 +1,31 @@ +--- +title: "Instantanés : un widget en SVG ou en PNG" +linkTitle: "Instantanés" +weight: 60 +description: "go-widgets/svg enveloppe n'importe quel rendu RGBA dans un SVG autonome, exact au bit près, ou l'écrit en PNG." +tags: [surfaces, svg, png] +--- + +[`go-widgets/svg`](https://github.com/go-widgets/svg) transforme un tampon RGBA en +SVG autonome : une `viewBox` calée sur la taille en pixels du rendu, et les +pixels sous forme de PNG en base 64 avec `image-rendering: pixelated`. Chaque pixel affiché +est le pixel que le widget a dessiné — pas de revectorisation, pas de substitution de police — et +il reste net à tout niveau de zoom. + +```go +// any RGBA producer +svg.Snapshot(f, surface, w, h, "my image") + +// a toolkit widget +btn := toolkit.NewButton("Click me", nil) +btn.SetBounds(toolkit.Rect{W: 200, H: 40}) +widget.Snapshot(f, btn, 200, 40, toolkit.DefaultLight(), "widget: button") +widget.PNG(p, btn, 200, 40, toolkit.DefaultLight()) +``` + +Le libellé devient le `<title>` et l'`aria-label` du SVG, échappés. Le paquet +racine ne dépend de rien ; le sous-paquet `widget` est séparé pour qu'un consommateur +doté de son propre producteur de pixels n'entraîne pas la boîte à outils dans son graphe de modules. + +`svg/cmd/gallery-render` rend le catalogue de widgets, en clair et en sombre : +[go-widgets.github.io/svg](https://go-widgets.github.io/svg/). diff --git a/content/fr/surfaces/terminal.md b/content/fr/surfaces/terminal.md new file mode 100644 index 0000000..8108575 --- /dev/null +++ b/content/fr/surfaces/terminal.md @@ -0,0 +1,64 @@ +--- +title: "Un terminal" +linkTitle: "Terminal" +weight: 40 +description: "go-widgets/tui rend les widgets à travers le painter en cellules : un moteur de rendu d'instantanés, un exécuteur interactif, et des widgets dimensionnés pour une grille de caractères." +tags: [surfaces, tui, terminal] +--- + +[`go-widgets/tui`](https://github.com/go-widgets/tui) transforme un arbre de widgets en +image de terminal à travers le [painter en cellules]({{< relref "/rendering/painter.md" >}}). +Bibliothèque standard uniquement — pas de `x/term`, pas de cgo. + +## Une image {#one-frame} + +```go +_ = tui.RenderOnce(os.Stdout, widgets, nil) // nil theme: light +``` + +`RenderOnceSized(w, cols, rows, widgets, theme)` prend la taille explicitement ; +`RenderOnce` lit `COLUMNS` et `LINES` et se replie sur 80×24. Lire +l'environnement plutôt qu'appeler `TIOCGWINSZ` garde le paquet exempt de +code propre à chaque plateforme. + +## Un programme interactif {#an-interactive-program} + +```go +app := tui.NewApp() +app.Root = buildScene() +app.Keys["q"] = func(a *tui.App) { a.Quit() } +app.Keys["Ctrl+C"] = func(a *tui.App) { a.Quit() } +os.Exit(app.Run()) +``` + +`Run` passe sur l'écran alternatif et en mode brut, distribue les entrées à `Root` +(les gestionnaires de touches globaux d'abord), suit `SIGWINCH`, et restaure toujours le +terminal — y compris lors d'une panique. Un gestionnaire de touche appelle `Consume` pour soustraire un événement à +`Root` ; `InputTarget` envoie chaque événement non consommé à un seul widget, ce qui permet +à une palette de commandes d'avaler la frappe tant qu'elle est ouverte. `NewFocusRing` donne à un +formulaire le parcours par Tab et Maj+Tab. + +## Des widgets dimensionnés pour les cellules {#widgets-sized-for-cells} + +La plupart des widgets de la boîte à outils sont conçus en pixels : leurs constantes de marge comptent +des cellules sous le painter en cellules et gonflent. `tui` a donc son propre jeu **natif en cellules** +— `TextEditor`, `Entry`, `Button`, `CheckButton`, `RadioButton`, `Scale`, +`ProgressBar`, `Sparkline`, `ListBox`, `TreeView`, `Table`, `MenuBar`, +`Notebook`, `HSplit`, `VSplit`, `Dialog`, `Dropdown`, `Spinner` et d'autres — +où un glyphe occupe une cellule. Chacun reste un `toolkit.Widget`, si bien que la même +instance dessine aussi en pixels. + +`TextEditor` est un éditeur de code en lecture-écriture : gouttière, annuler et rétablir, rechercher et +remplacer, sélection, déplacement de lignes, et coloration syntaxique pour Go (via +`go/scanner`), JavaScript et TypeScript, Python, Ruby, shell, C et C++, +Rust, JSON, YAML, HCL, TOML, LaTeX et Markdown, sans aucune dépendance externe. + +```sh +go run ./cmd/tui-widgets | less -R # every cell-native widget, one per slot +go run ./cmd/tui-widget-explorer # the same, interactive +go run ./cmd/tui-explorer # a file browser +go run ./cmd/tui-editor --file=x.go # a modal editor +``` + +Les deux programmes de démonstration sont testés de bout en bout dans un vrai pty : de vrais octets de touches en entrée, +l'image rendue vérifiée. diff --git a/docs/assets/logo.svg b/docs/assets/logo.svg deleted file mode 100644 index 3e545a5..0000000 --- a/docs/assets/logo.svg +++ /dev/null @@ -1,6 +0,0 @@ -<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg"> - <rect x="2" y="2" width="60" height="60" rx="12" fill="#0d9488"/> - <rect x="12" y="14" width="40" height="8" rx="2" fill="#ffffff" opacity="0.95"/> - <rect x="12" y="28" width="28" height="8" rx="2" fill="#ffffff" opacity="0.82"/> - <rect x="12" y="42" width="40" height="8" rx="2" fill="#ffffff" opacity="0.70"/> -</svg> diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index b21248e..0000000 --- a/docs/index.md +++ /dev/null @@ -1,28 +0,0 @@ -# go-widgets - -The widget toolkit and every surface it paints on: the painter seam, MVVM, a declarative skin engine, a terminal back-end, a native window on X11, Wayland, Cocoa and Win32, application lifecycle with a system tray, a real Android APK, and a desktop shell. - -Part of the **Desktop & widgets** family of the -[pure-Go ecosystem](https://go-desktop.github.io/) — 19 modules, -all `CGO_ENABLED=0`. - -## What is here - -This site is the organisation's reference index: what each module is, and where its -API documentation lives. The API itself is generated from the source and served by -[pkg.go.dev](https://pkg.go.dev/), which is always current with the released tags — -duplicating it here would only create a second copy to go stale. - -- **[Modules](modules.md)** — every module in go-widgets, with its source and its reference. - -## What every module here is held to - -- `CGO_ENABLED=0`: no cgo, and no shelling out to a command-line tool in place of a - library. -- Built and tested on amd64, arm64, riscv64, loong64, ppc64le and s390x — the last - being big-endian, which keeps every on-disk and on-wire encoding honest. -- 100% statement coverage as a CI gate, error branches included. -- BSD-3-Clause. - -The standard is described in full on the -[ecosystem map](https://go-desktop.github.io/docs/latest/standards/). diff --git a/docs/modules.md b/docs/modules.md deleted file mode 100644 index 8c12abc..0000000 --- a/docs/modules.md +++ /dev/null @@ -1,29 +0,0 @@ -# Modules - -The 19 modules of **go-widgets**. Each links to its source and to -its generated API reference. - -| Module | Reference | -| --- | --- | -| [`android`](https://github.com/go-widgets/android) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/android) | -| [`app-template`](https://github.com/go-widgets/app-template) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/app-template) | -| [`application`](https://github.com/go-widgets/application) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/application) | -| [`bricolint`](https://github.com/go-widgets/bricolint) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/bricolint) | -| [`data`](https://github.com/go-widgets/data) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/data) | -| [`desktop`](https://github.com/go-widgets/desktop) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/desktop) | -| [`gallery`](https://github.com/go-widgets/gallery) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/gallery) | -| [`isoicons`](https://github.com/go-widgets/isoicons) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/isoicons) | -| [`mvvm`](https://github.com/go-widgets/mvvm) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/mvvm) | -| [`mvvmlint`](https://github.com/go-widgets/mvvmlint) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/mvvmlint) | -| [`mvvmtk`](https://github.com/go-widgets/mvvmtk) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/mvvmtk) | -| [`painter`](https://github.com/go-widgets/painter) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/painter) | -| [`skin`](https://github.com/go-widgets/skin) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/skin) | -| [`svg`](https://github.com/go-widgets/svg) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/svg) | -| [`toolkit`](https://github.com/go-widgets/toolkit) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/toolkit) | -| [`tray`](https://github.com/go-widgets/tray) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/tray) | -| [`tui`](https://github.com/go-widgets/tui) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/tui) | -| [`webcanvas`](https://github.com/go-widgets/webcanvas) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/webcanvas) | -| [`window`](https://github.com/go-widgets/window) | [pkg.go.dev](https://pkg.go.dev/github.com/go-widgets/window) | - -Descriptions live on the repositories themselves rather than being copied here, so -there is one place to correct when one changes. diff --git a/hugo.yaml b/hugo.yaml new file mode 100644 index 0000000..a9f2d39 --- /dev/null +++ b/hugo.yaml @@ -0,0 +1,234 @@ +# go-widgets documentation (Hugo + Hextra), laid out like go-fileshare's. +# +# Each documentation version is a separate build under +# https://go-widgets.github.io/docs/<version>/. go-widgets is nineteen modules +# released on their own, so no single module's version can label the site: +# <version> is the documentation's own minor version (params.docs.version +# below: v0.2.0 -> 0.2), the label format mike used when this site was built +# with MkDocs. The CI sets it through --baseURL; this default is the same build. +baseURL: "https://go-widgets.github.io/docs/0.2/" +title: "go-widgets" +# themes/hextra is the tannevaled/hextra fork, pinned at a release tag: +# upstream Hextra plus opt-in features (partial menu items, combos, +# related-sites menu, page subtitle and history, darkreaderLock). +theme: hextra + +# English is the default language and stays at the root of each version +# (/docs/<version>/rendering/painter/), so the URL MkDocs published +# (/docs/latest/modules/) keeps working; the other languages live under their +# code (/docs/<version>/fr/rendering/painter/). The version MkDocs built (0.1) +# is English only: the version selector falls back to its English page. +defaultContentLanguage: en +defaultContentLanguageInSubdir: false + +# Each language has its own content directory, with the same file names, so +# that a page and its translation are paired. A language is declared here only +# once its content exists: a declared language without content would be an +# empty entry in the language switch. The flags are lipis/flag-icons' 4x3 +# flags (MIT, static/images/flags/LICENSE). +languages: + en: + label: "English" + contentDir: content/en + weight: 1 + params: + flag: images/flags/gb.svg + fr: + label: "Français" + contentDir: content/fr + weight: 2 + params: + flag: images/flags/fr.svg + # Day before month: "5 octobre 2026". + dateFormat: "2 January 2006" + description: "Une boîte à outils graphique en Go pur, et toutes les surfaces sur lesquelles elle peint : une fenêtre native sur X11, Wayland, macOS et Windows, un canvas de navigateur, un terminal, un APK Android. CGO_ENABLED=0." + es: + label: "Español" + contentDir: content/es + weight: 3 + params: + flag: images/flags/es.svg + # Day, then "de" month "de" year: "5 de octubre de 2026". + dateFormat: "2 de January de 2006" + description: "Un conjunto de widgets en Go puro y todas las superficies sobre las que pinta: una ventana nativa en X11, Wayland, macOS y Windows, un canvas de navegador, un terminal, un APK de Android. CGO_ENABLED=0." + de: + label: "Deutsch" + contentDir: content/de + weight: 4 + params: + flag: images/flags/de.svg + # Day before month: "5. Oktober 2026". + dateFormat: "2. January 2006" + description: "Ein Widget-Toolkit in reinem Go und alle Flächen, auf die es zeichnet: ein natives Fenster unter X11, Wayland, macOS und Windows, ein Browser-Canvas, ein Terminal, eine Android-APK. CGO_ENABLED=0." + +enableRobotsTXT: true +# Git history dates every page: "Last updated on" at the bottom of each page. +enableGitInfo: true + +markup: + goldmark: + renderer: + unsafe: true + parser: + # A translated heading keeps the English one's anchor with an explicit + # {#id}; attributes are needed for those. + attribute: + title: true + highlight: + noClasses: false + +module: + hugoVersion: + extended: false + min: 0.146.0 + # The mark and favicons come from the go-widgets brand repository + # (submodule branding/, pinned at a release tag), published under + # /images/brand/. + mounts: + - source: static + target: static + - source: assets + target: assets + - source: branding/svg/color + target: static/images/brand/svg + files: ["go-widgets.svg"] + - source: branding/png/color + target: static/images/brand/png + files: ["*/go-widgets.png"] + - source: branding/ico + target: static/images/brand/ico + files: ["go-widgets.ico"] + # The marks' licence (BSD-3-Clause) travels with them. + - source: branding + target: static/images/brand + files: ["LICENSE"] + +menu: + main: + # Navbar, left to right: the sibling projects, version, theme, search, + # sources. The siblings are the organisations this one paints through: + # go-opentype sets its text, go-macos is the Objective-C bridge the Cocoa + # back-end and the tray use, go-freedesktop is what the desktop shell + # composes, wasmdesk is the browser compositor the wasmbox back-end joins. + - name: Projects + identifier: projects + weight: 2 + - name: go-opentype + parent: projects + url: https://go-opentype.github.io/ + weight: 1 + - name: go-macos + parent: projects + url: https://go-macos.github.io/ + weight: 2 + - name: go-freedesktop + parent: projects + url: https://go-freedesktop.github.io/ + weight: 3 + - name: wasmdesk + parent: projects + url: https://wasmdesk.github.io/ + weight: 4 + - name: Version + weight: 3 + params: + type: partial + partial: custom/version-select.html + - name: Theme + weight: 4 + params: + type: theme-toggle + combo: true + - name: Search + weight: 5 + params: + type: search + - name: GitHub + url: https://github.com/go-widgets + weight: 6 + params: + icon: github + +params: + description: "A pure-Go widget toolkit and every surface it paints on: a native window on X11, Wayland, macOS and Windows, a browser canvas, a terminal, an Android APK. CGO_ENABLED=0." + + # The documentation's own release. Its minor version is the version label + # (v0.2.0 -> 0.2): the CI reads it here. Raise the minor when a version's + # pages should stay readable as they were; a patch replaces them in place. + docs: + version: "v0.2.0" + + # The language switch (foot of the sidebar) shows the translate icon, then + # the language's flag. It appears once a second language is declared. + languageSwitch: + icon: translate + + navbar: + displayTitle: true + displayLogo: true + logo: + # The teal mark reads on both themes; the name is the site title. + path: images/brand/svg/go-widgets.svg + dark: images/brand/svg/go-widgets.svg + width: 32 + height: 32 + # Related sites, opened from the chevron beside the title (the logo itself + # leads to the documentation's home). The landing page and the two live + # demos: the GitHub organisation is already the navbar's last item. + menu: + - name: go-widgets + url: https://go-widgets.github.io/ + logo: images/brand/svg/go-widgets.svg + - name: Gallery (live demo) + url: https://go-widgets.github.io/gallery/ + logo: images/brand/svg/go-widgets.svg + - name: SVG snapshots + url: https://go-widgets.github.io/svg/ + logo: images/brand/svg/go-widgets.svg + + theme: + default: system + # The theme toggle is in the navbar (menu above), not at the foot of the sidebar. + displayToggle: false + # Dark Reader would darken the light theme itself; with the lock, the dark + # theme is our own. + darkreaderLock: true + + footer: + enable: true + displayCopyright: true + displayPoweredBy: false + + displayUpdatedDate: true + dateFormat: "January 2, 2006" + + editURL: + enable: true + base: "https://github.com/go-widgets/docs/edit/main/content" + + toc: + displayTags: true + + page: + width: normal + # The front matter description as a subtitle under the title. + displayDescription: true + # "Created on ... by ... · Modified on ... by ..." at the foot of each page: + # creation from data/pagehistory.json, which the build writes with + # themes/hextra/scripts/page-history.sh (it needs the full git history); + # modification from git (enableGitInfo). + history: true + contextMenu: + enable: false + + highlight: + copy: + enable: true + display: hover + + search: + enable: true + type: flexsearch + flexsearch: + index: content + tokenize: forward diff --git a/i18n/de.yaml b/i18n/de.yaml new file mode 100644 index 0000000..6ae6ef8 --- /dev/null +++ b/i18n/de.yaml @@ -0,0 +1,4 @@ +# Zeichenketten der Website, zusätzlich zu denen von Hextra (themes/hextra/i18n/de.yaml). +copyright: "© 2026 die go-widgets-Autoren · BSD-3-Clause" +version: "Version" +projects: "Projekte" diff --git a/i18n/en.yaml b/i18n/en.yaml new file mode 100644 index 0000000..93a7153 --- /dev/null +++ b/i18n/en.yaml @@ -0,0 +1,4 @@ +# Site strings beside Hextra's own (themes/hextra/i18n/en.yaml). +copyright: "© 2026 the go-widgets authors · BSD-3-Clause" +version: "Version" +projects: "Projects" diff --git a/i18n/es.yaml b/i18n/es.yaml new file mode 100644 index 0000000..937c476 --- /dev/null +++ b/i18n/es.yaml @@ -0,0 +1,4 @@ +# Cadenas del sitio, además de las de Hextra (themes/hextra/i18n/es.yaml). +copyright: "© 2026 los autores de go-widgets · BSD-3-Clause" +version: "Versión" +projects: "Proyectos" diff --git a/i18n/fr.yaml b/i18n/fr.yaml new file mode 100644 index 0000000..a1cceed --- /dev/null +++ b/i18n/fr.yaml @@ -0,0 +1,4 @@ +# Chaînes du site, en plus de celles de Hextra (themes/hextra/i18n/fr.yaml). +copyright: "© 2026 les auteurs de go-widgets · BSD-3-Clause" +version: "Version" +projects: "Projets" diff --git a/layouts/_partials/custom/version-select.html b/layouts/_partials/custom/version-select.html new file mode 100644 index 0000000..ad480be --- /dev/null +++ b/layouts/_partials/custom/version-select.html @@ -0,0 +1,119 @@ +{{- /* Navbar version selector, as in claimward/docs. + + Each documentation version is a separate build published under + <docs root>/<version>/, where <version> is the documentation's minor version + (0.2, ...), and its baseURL ends with that version. A build + cannot know the versions published after it, so the server renders only the + current one; in the browser the list is filled from <docs root>/versions.json, + which the deploy job rewrites in the format mike writes (version, title, + aliases, properties), so the MkDocs-built version (0.1) and the Hugo + builds read the same file. + + The site is ready for several languages: English at the root of a version, + the others under their code (<version>/fr/...). Each option opens, in that + version, the first of these that exists: the current page in the current + language; the same page in English (the MkDocs-built versions are English + only); the home in the current language; the version's home. A page that a + version does not have therefore never ends on a 404. The language prefix is + read from the home's permalink, not guessed from the URL. + + Built on Hextra's own navbar dropdown (assets/js/core/nav-menu.js), so its + keyboard handling applies. */ -}} +{{- $page := . -}} +{{- $basePath := strings.TrimSuffix "/" (urls.Parse site.BaseURL).Path -}} +{{- $current := path.Base $basePath -}} +{{- $docsRoot := printf "%s/" (strings.TrimSuffix "/" (path.Dir $basePath)) -}} +{{- $versionRoot := printf "%s%s/" $docsRoot $current -}} +{{- $rest := strings.TrimPrefix $versionRoot $page.RelPermalink -}} +{{- /* "fr/" in French, "" in English (the default language, at the root). */ -}} +{{- $langPrefix := strings.TrimPrefix $versionRoot site.Home.RelPermalink -}} +{{- /* The same page in the default language: same file names in every content/<lang>/. */ -}} +{{- $restDefault := cond (ne $langPrefix "") (strings.TrimPrefix $langPrefix $rest) $rest -}} +{{- $dev := site.Params.versions.development | default "dev" -}} +{{- $itemClass := "hx:text-gray-600 hx:hover:text-gray-800 hx:dark:text-gray-400 hx:dark:hover:text-gray-200 hx:relative hx:cursor-pointer hx:whitespace-nowrap hx:rounded-sm hx:py-1.5 hx:transition-colors hx:ltr:pl-3 hx:ltr:pr-9 hx:rtl:pr-3 hx:rtl:pl-9 hx:flex hx:items-center hx:gap-1 hx:hover:bg-gray-100 hx:dark:hover:bg-neutral-800" -}} +{{- $iconAttrs := `height="1em" class="hx:inline-block"` -}} +<div class="fs-version-select hx:relative" + data-docs-root="{{ $docsRoot }}" data-current="{{ $current }}" + data-rest="{{ $rest }}" data-rest-default="{{ $restDefault }}" + data-lang-prefix="{{ $langPrefix }}" data-dev="{{ $dev }}"> + <button type="button" data-state="closed" aria-expanded="false" aria-haspopup="menu" + title="{{ T "version" }}" aria-label="{{ T "version" }}: {{ $current }}" + class="hextra-nav-menu-toggle fs-version-toggle hx:cursor-pointer hx:text-sm hx:relative hx:whitespace-nowrap hx:p-2 hx:flex hx:items-center hx:gap-1 hx:text-gray-600 hx:hover:text-gray-800 hx:dark:text-gray-400 hx:dark:hover:text-gray-200"> + {{- partial "utils/icon.html" (dict "name" (cond (eq $current $dev) "beaker" "tag") "attributes" $iconAttrs) -}} + <span>{{ $current }}</span> + {{- partial "utils/icon.html" (dict "name" "chevron-down" "attributes" `height=12 class="hx:transition-transform hx:duration-200 hx:ease-in-out"`) -}} + </button> + <ul class="hextra-nav-menu-items hx:hidden hx:z-20 hx:max-h-64 hx:overflow-auto hx:rounded-lg hx:border hx:border-gray-200 hx:bg-white hx:p-1 hx:text-sm hx:shadow-lg hx:dark:border-neutral-700 hx:dark:bg-neutral-900" + style="min-width: 100%;" role="menu"> + <li role="none" class="hextra-nav-menu-item hx:flex hx:flex-col"> + <a href="{{ $page.RelPermalink }}" role="menuitem" aria-current="true" class="{{ $itemClass }}"> + {{- partial "utils/icon.html" (dict "name" (cond (eq $current $dev) "beaker" "tag") "attributes" $iconAttrs) -}} + <span>{{ $current }}</span> + <span class="hx:absolute hx:inset-y-0 hx:flex hx:items-center hx:ltr:right-3 hx:rtl:left-3"> + {{- partial "utils/icon" (dict "name" "check" "attributes" "height=1em width=1em") -}} + </span> + </a> + </li> + </ul> + <template data-icon="development">{{ partial "utils/icon.html" (dict "name" "beaker" "attributes" $iconAttrs) }}</template> + <template data-icon="release">{{ partial "utils/icon.html" (dict "name" "tag" "attributes" $iconAttrs) }}</template> + <template data-icon="check">{{ partial "utils/icon" (dict "name" "check" "attributes" "height=1em width=1em") }}</template> +</div> +<script> + (function () { + var box = document.currentScript.previousElementSibling; + var d = box.dataset; + var list = box.querySelector("ul"); + var itemClass = list.querySelector("a").className; + var icon = function (name) { return box.querySelector('template[data-icon="' + name + '"]').innerHTML; }; + var isDev = function (v) { return v.version === d.dev || !!(v.properties && v.properties.development); }; + + fetch(d.docsRoot + "versions.json", { cache: "no-cache" }) + .then(function (r) { return r.ok ? r.json() : Promise.reject(r.status); }) + .then(function (versions) { + if (!Array.isArray(versions) || versions.length === 0) return; + list.innerHTML = ""; + versions.forEach(function (v) { + if (!v || typeof v.version !== "string" || (v.properties && v.properties.hidden)) return; + var li = document.createElement("li"); + li.setAttribute("role", "none"); + li.className = "hextra-nav-menu-item hx:flex hx:flex-col"; + var a = document.createElement("a"); + a.setAttribute("role", "menuitem"); + a.className = itemClass; + a.href = d.docsRoot + v.version + "/" + d.rest; + a.innerHTML = icon(isDev(v) ? "development" : "release"); + var label = document.createElement("span"); + var aliases = Array.isArray(v.aliases) && v.aliases.length ? " (" + v.aliases.join(", ") + ")" : ""; + label.textContent = (v.title || v.version) + aliases; + a.appendChild(label); + if (v.version === d.current) { + a.setAttribute("aria-current", "true"); + var check = document.createElement("span"); + check.className = "hx:absolute hx:inset-y-0 hx:flex hx:items-center hx:ltr:right-3 hx:rtl:left-3"; + check.innerHTML = icon("check"); + a.appendChild(check); + } else { + // The page may not exist in that version, or not in this language: + // try the candidates in order, the version's home last. + var root = d.docsRoot + v.version + "/"; + var tries = [d.rest, d.restDefault, d.langPrefix].map(function (p) { return root + p; }) + .filter(function (u, i, all) { return u !== root && all.indexOf(u) === i; }); + a.addEventListener("click", function (e) { + e.preventDefault(); + var next = function (i) { + if (i >= tries.length) { window.location.href = root; return; } + fetch(tries[i], { method: "HEAD" }) + .then(function (r) { if (r.ok) { window.location.href = tries[i]; } else { next(i + 1); } }) + .catch(function () { next(i + 1); }); + }; + next(0); + }); + } + li.appendChild(a); + list.appendChild(li); + }); + }) + .catch(function () { /* no versions.json (local build): only the current version is listed */ }); + })(); +</script> diff --git a/layouts/_partials/favicons.html b/layouts/_partials/favicons.html new file mode 100644 index 0000000..10f1e3a --- /dev/null +++ b/layouts/_partials/favicons.html @@ -0,0 +1,8 @@ +{{- /* The favicons are the go-widgets brand's org mark (submodule branding/, + mounted under /images/brand/ by hugo.yaml) rather than files copied into + static/. Replaces Hextra's favicons.html, which expects them at the root. */ -}} +<link rel="icon" href="{{ "images/brand/svg/go-widgets.svg" | relURL }}" type="image/svg+xml" id="favicon-svg" /> +<link rel="icon" href="{{ "images/brand/ico/go-widgets.ico" | relURL }}" sizes="16x16 32x32" /> +<link rel="icon" href="{{ "images/brand/png/32/go-widgets.png" | relURL }}" type="image/png" sizes="32x32" /> +<link rel="icon" href="{{ "images/brand/png/16/go-widgets.png" | relURL }}" type="image/png" sizes="16x16" /> +<link rel="apple-touch-icon" href="{{ "images/brand/png/256/go-widgets.png" | relURL }}" sizes="256x256" /> diff --git a/mkdocs.yml b/mkdocs.yml deleted file mode 100644 index a42b8a9..0000000 --- a/mkdocs.yml +++ /dev/null @@ -1,46 +0,0 @@ -site_name: go-widgets docs -site_description: "The widget toolkit and every surface it paints on: the painter seam, MVVM, a declarative skin engine, a terminal back-end, a native window on X11, Wayland, Cocoa and Win32, application lifecycle with a system tray, a real Android APK, and a desktop shell." -site_url: https://go-widgets.github.io/docs/ -repo_url: https://github.com/go-widgets -repo_name: go-widgets -edit_uri: "" - -theme: - name: material - logo: assets/logo.svg - favicon: assets/logo.svg - features: - - navigation.sections - - navigation.top - - content.code.copy - - search.suggest - palette: - # First entry has no explicit light/dark, so it is the default: follow the system. - - media: "(prefers-color-scheme)" - toggle: { icon: material/brightness-auto, name: Switch to light mode } - - media: "(prefers-color-scheme: light)" - scheme: default - primary: teal - toggle: { icon: material/weather-night, name: Switch to dark mode } - - media: "(prefers-color-scheme: dark)" - scheme: slate - primary: teal - toggle: { icon: material/brightness-auto, name: Follow the system } - -extra: - version: - provider: mike - default: latest - -nav: - - Overview: index.md - - Modules: modules.md - -markdown_extensions: - - admonition - - attr_list - - tables - - toc: { permalink: true } - - pymdownx.highlight: { anchor_linenums: true } - - pymdownx.superfences - - pymdownx.inlinehilite diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..22a9943 --- /dev/null +++ b/renovate.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:recommended"] +} diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 34eb296..0000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -mkdocs-material==9.7.7 -mike==2.2.0 diff --git a/scripts/publish-version.sh b/scripts/publish-version.sh new file mode 100755 index 0000000..ed00a53 --- /dev/null +++ b/scripts/publish-version.sh @@ -0,0 +1,82 @@ +#!/bin/sh +# Publish one documentation build into a checkout of the gh-pages branch. +# +# scripts/publish-version.sh <version> <built site> <gh-pages checkout> +# +# <version> is the minor version of the documentation, MAJOR.MINOR +# (0.2): the label format mike used for this site, so every URL it published +# keeps its meaning. The build replaces <gh-pages>/<version>/ and nothing else: every +# other version already published -- the MkDocs-built 0.1 among them -- +# is kept as it is. Then, from the directories present: +# +# versions.json rewritten in mike's format ({version, title, aliases}), +# newest first; the version selector of the Hugo builds and +# that of the MkDocs builds both read it; +# latest a symbolic link to the newest version (as mike made it); +# index.html the root, redirecting to latest/ (as mike made it). +# +# Needs jq and rsync. Commits nothing: the workflow does. +set -eu + +die() { echo "publish-version.sh: $*" >&2; exit 1; } + +[ $# -eq 3 ] || die "usage: $0 <version> <built site> <gh-pages checkout>" +version=$1 +site=$2 +pages=$3 +label='^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$' + +printf '%s\n' "$version" | grep -Eq "$label" || die "version must be MAJOR.MINOR (0.2), not \"$version\"" +[ -f "$site/index.html" ] || die "$site/index.html is missing: build the site first" +[ -d "$pages" ] || die "$pages is not a directory" +command -v jq >/dev/null || die "jq is required" +command -v rsync >/dev/null || die "rsync is required" + +# The version's own directory, replaced whole. A real directory: if it was a +# link (an alias), the link goes, not what it pointed to. +[ -L "$pages/$version" ] && rm "$pages/$version" +mkdir -p "$pages/$version" +rsync -a --delete "$site/" "$pages/$version/" + +# The versions present, newest first (0.12 before 0.2: compared as numbers). +versions=$(cd "$pages" && for d in *; do + [ -d "$d" ] && [ ! -L "$d" ] && printf '%s\n' "$d" | grep -Eq "$label" && printf '%s\n' "$d" +done | sort -t. -k1,1nr -k2,2nr) +latest=$(printf '%s\n' "$versions" | sed -n 1p) + +# versions.json: the newest carries the alias "latest". +for v in $versions; do + if [ "$v" = "$latest" ]; then + jq -n --arg v "$v" '{version: $v, title: $v, aliases: ["latest"]}' + else + jq -n --arg v "$v" '{version: $v, title: $v, aliases: []}' + fi +done | jq -s . >"$pages/versions.json.tmp" +mv "$pages/versions.json.tmp" "$pages/versions.json" + +# latest -> the newest version, and the root redirect. +rm -rf "$pages/latest" +ln -s "$latest" "$pages/latest" +cat >"$pages/index.html" <<'EOF' +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Redirecting + + + + + Redirecting to latest/... + + +EOF +touch "$pages/.nojekyll" + +echo "published $version; latest is $latest; versions: $(jq -r '[.[].version] | join(", ")' "$pages/versions.json")" diff --git a/static/images/flags/LICENSE b/static/images/flags/LICENSE new file mode 100644 index 0000000..ee959dc --- /dev/null +++ b/static/images/flags/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013 Panayiotis Lipiridis + +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/static/images/flags/de.svg b/static/images/flags/de.svg new file mode 100644 index 0000000..71aa2d2 --- /dev/null +++ b/static/images/flags/de.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/static/images/flags/es.svg b/static/images/flags/es.svg new file mode 100644 index 0000000..a296ebf --- /dev/null +++ b/static/images/flags/es.svg @@ -0,0 +1,544 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/images/flags/fr.svg b/static/images/flags/fr.svg new file mode 100644 index 0000000..4110e59 --- /dev/null +++ b/static/images/flags/fr.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/static/images/flags/gb.svg b/static/images/flags/gb.svg new file mode 100644 index 0000000..7991383 --- /dev/null +++ b/static/images/flags/gb.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/themes/hextra b/themes/hextra new file mode 160000 index 0000000..1d8daa4 --- /dev/null +++ b/themes/hextra @@ -0,0 +1 @@ +Subproject commit 1d8daa42d0406250fa00b7e803db2ab104a7fcab