From 218ea675186e2ec229743a6cc1e0967b59062229 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Fri, 9 Oct 2026 15:12:29 +0200 Subject: [PATCH 1/2] Move the documentation to Hugo and Hextra, laid out like go-fileshare's MkDocs and mike are replaced by the stack go-fileshare/docs uses: Hugo with the tannevaled/hextra fork, the org mark mounted from go-widgets/brand at v0.1.0, a version selector reading mike's versions.json, and four languages (English at the root, French, Spanish and German under their codes). The site said what each module is and linked to pkg.go.dev, in two pages. It now explains the stack: the painter seam, the toolkit, skins, MVVM and the data spine, every surface (window back-ends, application and tray, browser, terminal, Android, snapshots), the two CI rules, the modules, and a status page saying what is proven live, what is only compiled and what CI does not cover. Facts were checked against the modules' code at their newest tags; where a README disagrees with its code, the pages follow the code. Two claims of the old home page were false and are not carried over: every module does not have a 100% coverage gate (mvvmtk has none), and every module does not cross-compile for six architectures (twelve do). 0.1, the MkDocs build, stays on gh-pages as it was. /docs/latest/modules/ keeps working. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/docs.yml | 188 +++++-- .gitignore | 11 +- .gitmodules | 6 + README.md | 137 +++++ assets/css/custom.css | 30 + branding | 1 + content/de/_index.md | 76 +++ content/de/getting-started.md | 79 +++ content/de/guard-rails/_index.md | 24 + content/de/guard-rails/bricolint.md | 53 ++ content/de/guard-rails/mvvmlint.md | 44 ++ content/de/modules.md | 46 ++ content/de/rendering/_index.md | 29 + content/de/rendering/painter.md | 71 +++ content/de/rendering/skin.md | 81 +++ content/de/rendering/toolkit.md | 74 +++ content/de/state/_index.md | 23 + content/de/state/data.md | 45 ++ content/de/state/mvvm.md | 95 ++++ content/de/status.md | 64 +++ content/de/surfaces/_index.md | 31 ++ content/de/surfaces/android.md | 63 +++ content/de/surfaces/application.md | 82 +++ content/de/surfaces/browser.md | 54 ++ content/de/surfaces/native-window.md | 90 +++ content/de/surfaces/snapshots.md | 31 ++ content/de/surfaces/terminal.md | 64 +++ content/en/_index.md | 76 +++ content/en/getting-started.md | 79 +++ content/en/guard-rails/_index.md | 24 + content/en/guard-rails/bricolint.md | 53 ++ content/en/guard-rails/mvvmlint.md | 44 ++ content/en/modules.md | 47 ++ content/en/rendering/_index.md | 29 + content/en/rendering/painter.md | 71 +++ content/en/rendering/skin.md | 81 +++ content/en/rendering/toolkit.md | 74 +++ content/en/state/_index.md | 23 + content/en/state/data.md | 45 ++ content/en/state/mvvm.md | 95 ++++ content/en/status.md | 64 +++ content/en/surfaces/_index.md | 31 ++ content/en/surfaces/android.md | 64 +++ content/en/surfaces/application.md | 82 +++ content/en/surfaces/browser.md | 54 ++ content/en/surfaces/native-window.md | 90 +++ content/en/surfaces/snapshots.md | 31 ++ content/en/surfaces/terminal.md | 64 +++ content/es/_index.md | 76 +++ content/es/getting-started.md | 81 +++ content/es/guard-rails/_index.md | 25 + content/es/guard-rails/bricolint.md | 55 ++ content/es/guard-rails/mvvmlint.md | 44 ++ content/es/modules.md | 48 ++ content/es/rendering/_index.md | 30 + content/es/rendering/painter.md | 75 +++ content/es/rendering/skin.md | 86 +++ content/es/rendering/toolkit.md | 77 +++ content/es/state/_index.md | 24 + content/es/state/data.md | 46 ++ content/es/state/mvvm.md | 98 ++++ content/es/status.md | 66 +++ content/es/surfaces/_index.md | 32 ++ content/es/surfaces/android.md | 68 +++ content/es/surfaces/application.md | 85 +++ content/es/surfaces/browser.md | 56 ++ content/es/surfaces/native-window.md | 94 ++++ content/es/surfaces/snapshots.md | 32 ++ content/es/surfaces/terminal.md | 68 +++ content/fr/_index.md | 76 +++ content/fr/getting-started.md | 79 +++ content/fr/guard-rails/_index.md | 24 + content/fr/guard-rails/bricolint.md | 53 ++ content/fr/guard-rails/mvvmlint.md | 44 ++ content/fr/modules.md | 47 ++ content/fr/rendering/_index.md | 29 + content/fr/rendering/painter.md | 71 +++ content/fr/rendering/skin.md | 81 +++ content/fr/rendering/toolkit.md | 74 +++ content/fr/state/_index.md | 23 + content/fr/state/data.md | 45 ++ content/fr/state/mvvm.md | 95 ++++ content/fr/status.md | 64 +++ content/fr/surfaces/_index.md | 31 ++ content/fr/surfaces/android.md | 64 +++ content/fr/surfaces/application.md | 82 +++ content/fr/surfaces/browser.md | 54 ++ content/fr/surfaces/native-window.md | 89 +++ content/fr/surfaces/snapshots.md | 31 ++ content/fr/surfaces/terminal.md | 64 +++ docs/assets/logo.svg | 6 - docs/index.md | 28 - docs/modules.md | 29 - hugo.yaml | 234 ++++++++ i18n/de.yaml | 4 + i18n/en.yaml | 4 + i18n/es.yaml | 4 + i18n/fr.yaml | 4 + layouts/_partials/custom/version-select.html | 119 ++++ layouts/_partials/favicons.html | 8 + mkdocs.yml | 46 -- renovate.json | 4 + requirements.txt | 2 - scripts/publish-version.sh | 82 +++ static/images/flags/LICENSE | 21 + static/images/flags/de.svg | 5 + static/images/flags/es.svg | 544 +++++++++++++++++++ static/images/flags/fr.svg | 5 + static/images/flags/gb.svg | 7 + themes/hextra | 1 + 110 files changed, 6301 insertions(+), 155 deletions(-) create mode 100644 .gitmodules create mode 100644 README.md create mode 100644 assets/css/custom.css create mode 160000 branding create mode 100644 content/de/_index.md create mode 100644 content/de/getting-started.md create mode 100644 content/de/guard-rails/_index.md create mode 100644 content/de/guard-rails/bricolint.md create mode 100644 content/de/guard-rails/mvvmlint.md create mode 100644 content/de/modules.md create mode 100644 content/de/rendering/_index.md create mode 100644 content/de/rendering/painter.md create mode 100644 content/de/rendering/skin.md create mode 100644 content/de/rendering/toolkit.md create mode 100644 content/de/state/_index.md create mode 100644 content/de/state/data.md create mode 100644 content/de/state/mvvm.md create mode 100644 content/de/status.md create mode 100644 content/de/surfaces/_index.md create mode 100644 content/de/surfaces/android.md create mode 100644 content/de/surfaces/application.md create mode 100644 content/de/surfaces/browser.md create mode 100644 content/de/surfaces/native-window.md create mode 100644 content/de/surfaces/snapshots.md create mode 100644 content/de/surfaces/terminal.md create mode 100644 content/en/_index.md create mode 100644 content/en/getting-started.md create mode 100644 content/en/guard-rails/_index.md create mode 100644 content/en/guard-rails/bricolint.md create mode 100644 content/en/guard-rails/mvvmlint.md create mode 100644 content/en/modules.md create mode 100644 content/en/rendering/_index.md create mode 100644 content/en/rendering/painter.md create mode 100644 content/en/rendering/skin.md create mode 100644 content/en/rendering/toolkit.md create mode 100644 content/en/state/_index.md create mode 100644 content/en/state/data.md create mode 100644 content/en/state/mvvm.md create mode 100644 content/en/status.md create mode 100644 content/en/surfaces/_index.md create mode 100644 content/en/surfaces/android.md create mode 100644 content/en/surfaces/application.md create mode 100644 content/en/surfaces/browser.md create mode 100644 content/en/surfaces/native-window.md create mode 100644 content/en/surfaces/snapshots.md create mode 100644 content/en/surfaces/terminal.md create mode 100644 content/es/_index.md create mode 100644 content/es/getting-started.md create mode 100644 content/es/guard-rails/_index.md create mode 100644 content/es/guard-rails/bricolint.md create mode 100644 content/es/guard-rails/mvvmlint.md create mode 100644 content/es/modules.md create mode 100644 content/es/rendering/_index.md create mode 100644 content/es/rendering/painter.md create mode 100644 content/es/rendering/skin.md create mode 100644 content/es/rendering/toolkit.md create mode 100644 content/es/state/_index.md create mode 100644 content/es/state/data.md create mode 100644 content/es/state/mvvm.md create mode 100644 content/es/status.md create mode 100644 content/es/surfaces/_index.md create mode 100644 content/es/surfaces/android.md create mode 100644 content/es/surfaces/application.md create mode 100644 content/es/surfaces/browser.md create mode 100644 content/es/surfaces/native-window.md create mode 100644 content/es/surfaces/snapshots.md create mode 100644 content/es/surfaces/terminal.md create mode 100644 content/fr/_index.md create mode 100644 content/fr/getting-started.md create mode 100644 content/fr/guard-rails/_index.md create mode 100644 content/fr/guard-rails/bricolint.md create mode 100644 content/fr/guard-rails/mvvmlint.md create mode 100644 content/fr/modules.md create mode 100644 content/fr/rendering/_index.md create mode 100644 content/fr/rendering/painter.md create mode 100644 content/fr/rendering/skin.md create mode 100644 content/fr/rendering/toolkit.md create mode 100644 content/fr/state/_index.md create mode 100644 content/fr/state/data.md create mode 100644 content/fr/state/mvvm.md create mode 100644 content/fr/status.md create mode 100644 content/fr/surfaces/_index.md create mode 100644 content/fr/surfaces/android.md create mode 100644 content/fr/surfaces/application.md create mode 100644 content/fr/surfaces/browser.md create mode 100644 content/fr/surfaces/native-window.md create mode 100644 content/fr/surfaces/snapshots.md create mode 100644 content/fr/surfaces/terminal.md delete mode 100644 docs/assets/logo.svg delete mode 100644 docs/index.md delete mode 100644 docs/modules.md create mode 100644 hugo.yaml create mode 100644 i18n/de.yaml create mode 100644 i18n/en.yaml create mode 100644 i18n/es.yaml create mode 100644 i18n/fr.yaml create mode 100644 layouts/_partials/custom/version-select.html create mode 100644 layouts/_partials/favicons.html delete mode 100644 mkdocs.yml create mode 100644 renovate.json delete mode 100644 requirements.txt create mode 100755 scripts/publish-version.sh create mode 100644 static/images/flags/LICENSE create mode 100644 static/images/flags/de.svg create mode 100644 static/images/flags/es.svg create mode 100644 static/images/flags/fr.svg create mode 100644 static/images/flags/gb.svg create mode 160000 themes/hextra 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..949e325 --- /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` | `https://go-widgets.github.io/docs/0.2/` | +| the newest version | also `https://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 From 554354b1ec1a05115a0ffde4b461e27fca1bf733 Mon Sep 17 00:00:00 2001 From: tannevaled Date: Fri, 9 Oct 2026 15:14:39 +0200 Subject: [PATCH 2/2] README: name the published versions without a scheme The links job fetches every https:// URL the README names, and /docs/0.2/ exists only once this branch has deployed: a check that cannot pass before the merge it is gating. The table describes where a version goes; it does not need to be a link. Co-Authored-By: Claude Opus 5.5 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 949e325..8e4afc7 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,8 @@ version**: | Source | Published under | | --- | --- | -| branch `main`, with `params.docs.version: v0.2.0` in `hugo.yaml` | `https://go-widgets.github.io/docs/0.2/` | -| the newest version | also `https://go-widgets.github.io/docs/latest/` | +| 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.