Cette page a été traduite automatiquement et peut contenir des erreurs. Lire l'original en anglais

Éléments

View

View est la brique de base de chaque écran : un conteneur natif (une UIView sur iOS et Mac Catalyst, un ViewGroup sur Android) qui dispose ses enfants avec flexbox ou grid, dessine un fond, des bordures, des ombres et des dégradés, et gère les appuis. Les boutons, les cartes, les lignes et les écrans sont tous des vues. Rust en crée une avec view(), TSX avec <View> depuis nsh.

el-view.tsx
el-view.tsxLive
import { mount, signal, Text, View } from "nsh";
import { button, buttonText, desktop, focusRing, Inspector, Panel, Stage, ui, wide } from "./palette";

function App() {
  const presses = signal(0);
  const size = signal("");
  const views = () => (
    <>
      <View
        onPress={() => presses.update((n) => n + 1)}
        {...button("accent", { align: "center" })}
      >
        <Text style={buttonText("accent")}>activeStyle opacity · pressed {presses()}×</Text>
      </View>

      <View
        onPress={() => presses.update((n) => n + 1)}
        activeStyle={{ backgroundColor: ui().border }}
        style={{ ...ui().card, backgroundImage: "none", borderRadius: 16, padding: 16 }}
        hoverStyle={{ opacity: 0.88 }}
        focusStyle={focusRing()}
      >
        <Text style={{ color: ui().text, fontSize: wide() ? 14 : 16 }}>activeStyle background ({desktop ? "click" : "tap"} and hold)</Text>
      </View>

      <View class="flex-row gap-3">
        <View class="flex-1 h-20 rounded-xl bg-[#ff6170] hover:bg-[#ff7d89] active:bg-[#d94d5b] items-center justify-center" focusStyle={focusRing()} onPress={() => {}}>
          <Text class="text-[#13160c] font-semibold">active:</Text>
        </View>
        <View class="flex-1 h-20 rounded-xl bg-[#45d49a] items-center justify-center shadow-lg">
          <Text class="text-[#0c0d0f] font-semibold">shadow-lg</Text>
        </View>
        <View
          class="flex-1 h-20 rounded-xl items-center justify-center"
          style={{ backgroundImage: `linear-gradient(135deg, ${ui().violet}, ${ui().info})` }}
        >
          <Text class="font-semibold" style={{ color: ui().page }}>gradient</Text>
        </View>
      </View>
    </>
  );
  const measured = (height?: number) => (
    <View
      onLayout={({ bounds }) => size.set(`${Math.round(bounds.size.width)} × ${Math.round(bounds.size.height)} pt`)}
      style={{
        flex: height === undefined ? 1 : undefined,
        height,
        borderRadius: 12,
        borderWidth: 2,
        borderStyle: "dashed",
        borderColor: ui().faint,
        alignItems: "center",
        justifyContent: "center",
      }}
    >
      <Text style={{ color: ui().muted }}>onLayout: {size()}</Text>
    </View>
  );

  return (
    <View style={{ flex: 1, backgroundColor: ui().page }}>
      {() =>
        wide() ? (
          <View style={{ flex: 1, padding: 20, paddingTop: 24, gap: 14 }}>
            <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>view</Text>
            <Text style={{ color: ui().muted }}>Click and hold the views; resize the window to see onLayout</Text>
            <View style={{ flex: 1, flexDirection: "row", gap: 16 }}>
              <Stage width={480} gap={16}>
                {views()}
                {measured(120)}
              </Stage>
              <Inspector>
                <Panel title="onPress">
                  <Text style={{ fontSize: 13, color: ui().text, fontVariant: ["tabular-nums"] }}>pressed {presses()}×</Text>
                </Panel>
                <Panel title="onLayout" grow>
                  <Text style={{ fontSize: 13, color: ui().text, fontVariant: ["tabular-nums"] }}>{size()}</Text>
                </Panel>
              </Inspector>
            </View>
          </View>
        ) : (
          <View style={{ flex: 1, width: "100%", maxWidth: 640, alignSelf: "center", padding: 20, paddingTop: 24, gap: 16 }}>
            <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>view</Text>
            {views()}
            {measured()}
          </View>
        )
      }
    </View>
  );
}

mount(App);
use nsh_core::prelude::*;

fn app() -> impl IntoElement {
    let liked = signal(false);
    view()
        .flex_1()
        .p_6()
        .gap_4()
        .bg(rgb(0xf6f8fa))
        .child(
            view()
                .flex_row()
                .items_center()
                .gap_3()
                .p_4()
                .rounded(px(16.0))
                .bg(white())
                .shadow_md()
                .child(view().size(px(40.0)).rounded_full().bg(rgb(0x8250df)))
                .child(text("A card").flex_1().text_base())
                .child(
                    view()
                        .hit_slop(EdgeInsets::all(px(8.0)))
                        .active(|s| s.opacity(0.5))
                        .on_press(move || liked.update(|l| *l = !*l))
                        .child(text(move || if liked.get() { "♥" } else { "♡" }).text_size(px(22.0))),
                ),
        )
}

#Disposition

Une vue dispose ses enfants avec flexbox, avec ces valeurs par défaut : flexDirection: "column", flexShrink: 0, alignContent: "flex-start", position: "relative". display: "grid" (.grid() en Rust) bascule vers CSS grid et display: "block" vers la disposition en bloc. Les tailles sont en points. Voir layout et la référence des styles.

display: "flex" suit CSS : dans un style, comme classe flex et comme .flex() de Rust, c’est une ligne sauf si une direction est définie (flexDirection: "column", flex-col, .flex_col()). Une vue qui ne définit pas de display empile ses enfants en colonne.

#Props

View a une seule prop qui lui est propre, selectable (.selectable(true) en Rust) : les textes à l’intérieur de la vue se sélectionnent ensemble, comme une seule sélection (Sélection de texte). Les autres sont les props communes : ses styles (style, class, classList ; les méthodes Styled en Rust), disabled, les props d’appui (onPress, onLongPress, onPressDown, onPressUp, delayLongPress, hitSlop), les styles d’état (activeStyle, hoverStyle, focusStyle, disabledStyle, checkedStyle), onMouseEnter / onMouseMove / onMouseLeave, onContextMenu, onFileDragEnter / onFileDragLeave / onFileDrop, focusable, onLayout, ref, nativeID, testID, les props de gestes (pan, pinch, rotation et leurs événements), les props de toucher et de responder, les props d’accessibilité et ios / android / desktop.

#Vues sélectionnables

Avec selectable, une sélection traverse les éléments Text et les TextView à l’intérieur de la vue, dans l’ordre de lecture ; les textes avec selectable={false} en sont exclus, et une vue sélectionnable placée dans une autre fait partie de la vue extérieure. Voir Sélection de texte pour les gestes sur chaque plateforme.

<View selectable class="gap-3 p-4">
  <Text>One selection runs from here…</Text>
  <Text>…to here.</Text>
</View>

#Retour visuel à l’appui

Une vue pressable est toute vue dotée d’un gestionnaire d’appui. Son retour visuel est défini par l’app : un style appliqué pendant l’appui, de façon native tant que le doigt est posé, sans aller-retour avec le code de l’app. hoverStyle, focusStyle et disabledStyle font de même pour les autres états :

// Fade the whole view.
view().on_press(open).active(|s| s.opacity(0.6))

// Show a background; group_active styles the children.
view()
    .group()
    .on_press(open)
    .active(|s| s.bg(rgb(0xe5e7eb)))
    .child(text("Open").group_active(|s| s.opacity(0.7)))

// Change any style while pressed.
view().on_press(open).bg(rgb(0x2563eb)).active(|s| s.bg(rgb(0x1e40af)).scale(0.95))

Sur Android, android={{ ripple: { color: "#0000001f" } }} (.ripple(Ripple { .. }) en Rust) ajoute l’effet de vague material (borderless, radius et foreground l’ajustent).

disabled retire les écouteurs d’appui de la vue native (pas d’appui, pas de retour visuel) et applique les styles désactivés :

view()
    .on_press(submit)
    .disabled(move || !valid.get())
    .bg(rgb(0x16a34a))
    .disabled_style(|s| s.opacity(0.4))

#Événements

ÉvénementRustDonnéesQuand
onPress.on_pressPressEvent (aucune en Rust)Un tap s’est terminé à l’intérieur de la vue.
onLongPress.on_long_pressPressEvent (aucune en Rust)Le doigt est resté posé pendant delayLongPress (500 ms).
onPressDown / onPressUp.on_press_down / .on_press_upPressEvent (aucune en Rust)Un doigt s’est posé / s’est levé ou a quitté la vue.
onMouseEnter / onMouseLeave.on_mouse_enter / .on_mouse_leaveMouseEventLa souris est entrée sur la vue (ou sur une vue à l’intérieur) / l’a quittée ou a quitté la fenêtre (survol) ; celui de onMouseLeave est l’endroit où elle se trouvait en dernier.
onMouseMove.on_mouse_moveMouseEventLa souris s’est déplacée sur la vue (ou sur une vue à l’intérieur), avec ou sans bouton enfoncé, ou une touche de modification a été pressée ou relâchée alors qu’elle y repose sans bouton enfoncé : position, pagePosition (Points), modifiers et pressedButton ("left", "right", "middle", "back", "forward" ou null) ; au plus une fois par frame.
onContextMenu.on_context_menuContextMenuEventLe bouton secondaire a été pressé sur la vue (menus contextuels).
onFileDragEnter / onFileDragLeave.on_file_drag_enter / .on_file_drag_leaveaucuneUn glissement de fichiers locaux est entré dans / a quitté la destination desktop active.
onFileDrop.on_file_drop{ uris: string[] } (FileDropEvent)Des fichiers ou dossiers locaux ont été déposés sur la destination desktop.
onLayout.on_layout{ bounds } (Bounds<Points>)Le cadre relatif au parent a changé.
onPanStart / onPanEnd.on_pan_start / .on_pan_endPanEventUn glissement lié avec pan a commencé / s’est terminé ; cancelled vaut true lorsque le système l’annule.
onPinchStart / onPinchEnd.on_pinch_start / .on_pinch_end{ scale, velocity }Un pincement lié avec pinch a commencé / s’est terminé.
onRotationStart / onRotationEnd.on_rotation_start / .on_rotation_end{ rotation, velocity }Une rotation liée avec rotation a commencé / s’est terminée.
onAccessibilityAction.on_accessibility_action{ actionName }Une action d’accessibilité personnalisée a été exécutée.

Pour les événements de toucher et de responder, voir les props communes.

#Dépôt de fichiers sur desktop

Sur AppKit, GTK, Win32 et GPUI, une vue avec onFileDrop reçoit les fichiers et dossiers locaux glissés depuis une autre application. onFileDragEnter et onFileDragLeave suivent la destination active ; onFileDrop reçoit { uris: string[] } dans l’ordre de la liste de fichiers native. Chaque entrée est une URL file: absolue, avec les espaces et les caractères non ASCII échappés. Analysez l’URL pour obtenir un chemin du système de fichiers plutôt que de retirer le préfixe à la main.

let hovering = signal(false);
let files = signal(Vec::<String>::new());

view()
    .min_h(px(160.0))
    .p_6()
    .on_file_drag_enter(move || hovering.set(true))
    .on_file_drag_leave(move || hovering.set(false))
    .on_file_drop(move |event: &FileDropEvent| {
        files.set(event.uris.clone());
        hovering.set(false);
    })
    .child(text(move || if hovering.get() { "Release files here" } else { "Drop files here" }))
    .child(text(move || files.get().join("\n")))

L’ancêtre actif le plus proche qui écoute onFileDrop reçoit le dépôt. Une racine modale borne cette recherche. disabled retire les écouteurs de glisser-déposer de fichiers ; la sortie, l’annulation, l’achèvement et le retrait de la destination effacent l’état actif. L’acceptation utilise l’opération Copy et l’état natif monté. C’est l’app qui décide de lire, d’ouvrir ou de transférer les fichiers reçus.

#Méthodes de ref

La ref d’une vue simple est un ViewRef : focus(), blur() et measure(). ref (.element_ref(&handle) en Rust) vous donne son nœud, par exemple pour le passer à setAccessibilityFocus (l’API d’accessibilité de nsh), ou pour la mesurer (handle.measure() en Rust).

#Vues dans un texte

Une vue placée dans un texte est une pièce jointe en ligne : elle se place sur la ligne de base, passe à la ligne avec les mots et reçoit ses propres appuis. Voir text.

#Notes par plateforme

  • boxShadow est dessinée sur toutes les plateformes ; android={{ elevation }} donne à la place l’ombre material d’Android. Sur Android, une ombre nécessite une couleur de fond (une vue au fond transparent n’en projette aucune).
  • filter sur les vues : iOS prend en charge opacity(), brightness() jusqu’à 1 et drop-shadow() (les images acceptent tous les filtres de couleur et blur()) ; Android les prend tous en charge à partir de l’API 31, et les filtres de couleur en dessous.
  • backdropFilter: "blur(…)" est un matériau système sur iOS (une approximation) et nécessite l’API 31 sur Android.
  • Android dessine les transformations translate, scale et rotate ; les inclinaisons (skew(), skewX(), skewY()) s’appliquent sur les autres plateformes.
  • borderCurve: "continuous" (.border_curve(BorderCurve::Continuous)) dessine des coins qui s’adoucissent vers les bords (comme les icônes d’app iOS), sur les deux plateformes.
  • Le style cursor s’applique là où il y a un pointeur (Mac Catalyst, iPad avec pointeur, desktops).