FrançaisEnglish

04 — Mise en page & thème

Comment les enfants sont disposés sur la page, comment faire en sorte que la mise en page occupe tout le viewport comme une application de bureau, et comment le thème de couleur du système d'exploitation pilote l'apparence visuelle.

Aide-mémoire des conteneurs

Conteneur Flux des enfants Utiliser quand
Element.Stack() Vertical, écart de 8 px Sections empilées verticalement, champs de formulaire, racine de page.
Element.Row() Horizontal, écart de 8 px, centrage vertical Widgets côte à côte, barres de boutons.
Element.Flow(dir) dir (« row » / « column ») avec retour à la ligne Nuage de tags, barre d'outils de boutons qui se replie sur les fenêtres étroites.
Element.Grid(r,c,g) CSS Grid — r × c cellules, écart gpx Mise en page tabulaire, cartes de tableau de bord. Passez r=0 pour des lignes implicites.
Element.AbsoluteContainer() Parent position:relative Mises en page de type diagramme / designer. Les enfants utilisent .Position(x, y).
Element.Div() Block, sans mise en page interne Quand vous avez besoin d'un wrapper stylé.
Element.Panel() Alias de Div Quand le nom WinForms se lit mieux.
Element.GroupBox(title) Block avec légende <legend> Sous-sections visuellement groupées d'un formulaire.
Element.TabControl(g) / Tab(g, id, label, body) Barre d'onglets + panneau actif en dessous Catégoriser de nombreux widgets sans surcharger l'utilisateur.
Element.SplitContainer(or, ratio) (v0.0.8) Deux volets redimensionnables ("row" ou "column") Shell de type IDE, interface maître/détail.
Element.MenuBar() Barre de menu en haut de la fenêtre (v0.0.8) Menus Fichier / Édition / Affichage au niveau de l'application.
Element.ToolStrip() Rangée de boutons horizontale avec thème Barre d'outils au-dessus du contenu.
Element.StatusStrip() Pied de page fixé en bas Ligne d'état / version / état de connexion.
Element.Stack()
    .AddChild(Element.GroupBox("Contact")
        .AddChild(Element.Input("name").Attr("placeholder", "Name"))
        .AddChild(Element.Input("email").Attr("placeholder", "Email")))
    .AddChild(Element.Row()
        .AddChild(Element.Button("Save"))
        .AddChild(Element.Button("Cancel")))

Element.Position(x, y) + .Size(w, h)

Pour un placement au pixel près à l'intérieur d'un AbsoluteContainer :

Element.AbsoluteContainer()
    .Size(0, 200)        // 200 px de hauteur, largeur fluide
    .AddChild(Element.Label("X").Position(40, 10).Size(80, 24))
    .AddChild(Element.Label("Y").Position(40, 60).Size(80, 24))

Position applique position:absolute;left:Xpx;top:Ypx. Cela ne prend effet qu'à l'intérieur d'un parent avec position:relative|absolute|fixed, ce que fournit précisément AbsoluteContainer.

Occupation du viewport

Par défaut depuis la v0.0.5, Page dispose le corps comme une application de bureau (mode FillViewport) :

Pour les documents longs qui doivent défiler naturellement, désactivez ce comportement :

Page.New().NaturalFlow().SetBody(...)

Pour le comportement de shell d'application (le défaut), aucun appel supplémentaire n'est nécessaire — structurez simplement votre arbre de sorte que le premier enfant du corps soit un conteneur qui organise le reste :

Page.New().SetBody(
    Element.Stack()
        .AddChild(Element.Heading("My App"))
        .AddChild(Element.TabControl("main")
            .AddChild(Element.Tab("main", "a", "A", panelA))
            .AddChild(Element.Tab("main", "b", "B", panelB)))
        .AddChild(Element.StatusStrip()
            .AddChild(Element.Label("Ready")))
)

TabControl reçoit automatiquement flex:1; min-height:0 depuis sa classe de base, il grandit donc pour remplir son parent. Le panneau de l'onglet actif a overflow:auto. StatusStrip est en position:fixed et ne consomme pas d'espace dans la mise en page — il flotte en bas du viewport.

Page.FullBleed() (v0.0.8) — supprimer le padding du corps

FillViewport conserve le padding de corps par défaut de 16 px pour que les formulaires simples aient de l'air. Les mises en page de type shell IDE où la MenuBar touche le bord supérieur et où un SplitContainer remplit le reste nécessitent un corps sans padding :

Page.New()
    .FullBleed()                            // body padding:0
    .SetBody(
        Element.Stack()
            .AddChild(Element.MenuBar()...) // touche le bord supérieur
            .AddChild(Element.SplitContainer("row", 25)
                .AddChild(treePane)
                .AddChild(editorPane)
                .Fill())
            .AddChild(Element.StatusStrip()...))

FullBleed et FillViewport sont indépendants — FullBleed ne modifie que le padding du corps ; FillViewport contrôle la hauteur de la mise en page. La combinaison des deux est le shell IDE typique.

.Fill() — désigner un enfant comme cible de flex-grow

Quand votre mise en page racine n'est pas un TabControl mais un autre contenu de grande hauteur (un Stack de champs, un long ListView, un SplitContainer), marquez l'enfant qui doit occuper l'espace restant et défiler :

Element.Stack()
    .AddChild(Element.Heading("Inbox"))             // hauteur naturelle
    .AddChild(Element.ToolStrip()                   // hauteur naturelle
        .AddChild(Element.Button("Refresh")))
    .AddChild(longListViewContainer.Fill())         // prend le reste
    .AddChild(Element.StatusStrip()                  // pied de page fixe
        .AddChild(Element.Label("Connected")))

.Fill() ajoute la classe amc-fill-child, que la feuille de style de base traduit par flex:1; min-height:0; overflow:auto.

Mécanique du TabControl

CSS pur — aucun JavaScript requis pour la commutation d'onglets.

Element.TabControl("settings")
    .AddChild(Element.Tab("settings", "general",  "General",  generalBody)
        .Attr("checked", "checked"))    // ← onglet actif par défaut
    .AddChild(Element.Tab("settings", "advanced", "Advanced", advancedBody))
    .AddChild(Element.Tab("settings", "about",    "About",    aboutBody))

Sous le capot : chaque Tab est un <label> enveloppant un <input type=radio> masqué, plus le corps du panneau. Tous les radios partagent le name (le groupe), de sorte que cocher l'un décoche les autres. Un sélecteur CSS de type sibling révèle uniquement le panneau de l'onglet :checked.

Mécanique du SplitContainer (v0.0.8)

Element.SplitContainer("row", 30)   // "row" → séparateur vertical
    .AddChild(leftPane)
    .AddChild(rightPane)

Deux volets plus un séparateur fin. Le séparateur est déplaçable : un petit pont JS câble pointerdownpointermove pour ajuster les valeurs flex-grow des volets → pointerup termine le glisser. Le ratio de division est limité à 5..95 pour qu'aucun volet ne puisse se réduire complètement.

L'orientation "column" donne un séparateur horizontal (volets haut/bas). Le conteneur gère son propre thème ; vous n'avez rien à styliser pour obtenir un séparateur fonctionnel en thème clair ou sombre.

MenuBar est simplement un élément — placez-le là où vous mettriez n'importe quel autre conteneur. L'emplacement habituel est le premier enfant du corps de la page :

Page.New().FullBleed().SetBody(
    Element.Stack()
        .AddChild(Element.MenuBar()
            .AddChild(Element.MenuItem("File")
                .AddChild(Element.MenuOption("New",  "amc_new"))
                .AddChild(Element.MenuOption("Quit", "amc_quit"))))
        .AddChild(mainContent.Fill())
        .AddChild(Element.StatusStrip()
            .AddChild(Element.Label("Ready"))))

Liez les noms d'action avec win.Bind("amc_new", handler). La MenuBar porte data-mode="common" aujourd'hui ; la v0.1.0 ajoutera un data-mode="native" optionnel qui bascule vers les menus natifs du système d'exploitation sans modifier votre code AM.

Thème — les sept variables CSS

La feuille de style de base expose sept variables qui basculent avec le thème du système d'exploitation. Surchargez-en une ou plusieurs depuis votre propre feuille de style :

Variable Défaut clair Défaut sombre Utilisée par
--amc-bg #fff #1e1e1e Fond du corps, fond des inputs
--amc-fg #1a1a1a #e8e8e8 Texte du corps, texte des contrôles
--amc-muted #6a6a6a #9a9a9a Texte des légendes, barre d'état
--amc-border #d0d0d0 #404040 Bordures des inputs, bordures des cellules de tableau
--amc-surface #f5f5f5 #2a2a2a Fond des boutons, fond <pre>, fond des barres d'outils, fond de la MenuBar
--amc-accent #0066cc #4a9eff Anneaux de focus, curseur du slider, bordure de l'onglet actif, contour du jour courant du MonthCalendar, liens
--amc-radius 4px 4px Border-radius des inputs et boutons

Le thème du système d'exploitation est détecté au moment du rendu et écrit dans <html data-theme="dark|light">. Surchargez par page :

Page.New().SetTheme("dark")    // forcer le thème sombre
Page.New().SetTheme("light")   // forcer le thème clair
Page.New().SetTheme("auto")    // défaut — suivre le système d'exploitation

Vous pouvez également surcharger via une variable d'environnement : AMALGAME_UI_THEME=dark l'emporte sur la détection du système d'exploitation au moment de l'exécution.

Basculement de thème en direct — Page.AutoTheme(true) (v0.0.8)

Par défaut, le thème du système d'exploitation est lu une seule fois au moment de Page.Render. Si l'utilisateur bascule entre le mode sombre et le mode clair pendant que l'application tourne, la page reste sur le thème qu'elle avait au chargement.

Activez AutoTheme pour que la page suive le système en direct :

Page.New()
    .AutoTheme(true)
    .SetBody( … )
    .ApplyTo(win)

Sous le capot, le pont JS écoute matchMedia('(prefers-color-scheme: dark)').onchange et bascule <html data-theme> en conséquence — les variables --amc-* suivent.

AutoTheme est optionnel pour des raisons de compatibilité ascendante — la v0.0.5 était livrée sans lui. Les versions futures pourraient en faire le comportement par défaut.

Page.OnThemeChange(handler) (v0.0.8)

Branchez-vous sur le même pont de thème en direct quand votre application a besoin de réagir au-delà du CSS (re-rendre une icône SVG, mettre à jour un indicateur Pre, journaliser le changement) :

Page.New()
    .OnThemeChange((req: string) => {
        // req est "light" ou "dark"
        Console.WriteLine("theme: " + req)
        return ""
    })
    .SetBody( … )

OnThemeChange implique AutoTheme(true). Le handler se déclenche également une fois au chargement initial de la page pour pouvoir synchroniser les widgets avec le thème courant.

Personnaliser les variables

Page.New()
    .AddCss("data:text/css,:root{--amc-accent:%23ff6b00;--amc-radius:8px}")

(# encodé en URL sous la forme %23 car l'URL est une data: URL.) Ou fournissez un vrai fichier de feuille de style :

Page.New()
    .AddCss("file:///abs/path/to/my-theme.css")

Trois façons d'intégrer du CSS :

// 1. Feuille de base + vos surcharges (superposées par-dessus)
Page.New().AddCss("file:///abs/app/overrides.css")

// 2. Ignorer la feuille de base, fournir votre propre feuille complète
Page.New().SetStylesheet("file:///abs/app/style.css")

// 3. Ignorer la feuille de base, superposer plusieurs feuilles
Page.New().NoBaseline()
    .AddCss("https://unpkg.com/@picocss/pico@2/css/pico.min.css")
    .AddCss("file:///abs/app/app.css")

Verrouillage du chrome navigateur

Par défaut, le menu contextuel du clic droit et les raccourcis de rechargement (F5, Ctrl+R / Cmd+R) de la webview sont bloqués. Ils amèneraient l'utilisateur sur about:blank car le document est chargé via SetHtml sans URL de support.

Les applications qui naviguent via Window.Navigate(url) vers une vraie URL et souhaitent retrouver le chrome navigateur standard peuvent le réactiver :

Page.New().AllowBrowserDefaults()

Les DevTools (Ctrl+Shift+I dans les builds de débogage) ne sont jamais bloqués.

Limites des chaînes fluentes

L'inférence de type d'amc avait un comportement quadratique sur les chaînes .AddChild(...) au-delà de ~24 liens. Corrigé dans amc v0.8.16 (mémoïsation sur InferTypeFromExpr indexée par l'identité du pointeur AST), les chaînes de n'importe quelle longueur sont donc maintenant linéaires en temps. Le pattern avec let intermédiaires reste le style recommandé de toute façon, car il se lit mieux :

let header: Element = Element.Stack()
    .AddChild(...)
let main:   Element = Element.Stack()
    .AddChild(...)
let footer: Element = Element.Stack()
    .AddChild(...)

Element.Stack()
    .AddChild(header)
    .AddChild(main)
    .AddChild(footer)

Si vous utilisez amc < 0.8.16, découpez les longues chaînes de la même façon comme solution de contournement.