本頁由機器翻譯,可能有錯誤。 閱讀英文原文

元素

View

View 是每個畫面的基本構件:一個原生容器(在 iOS 與 Mac Catalyst 上是 UIView,在 Android 上是 ViewGroup),以 flexbox 或 grid 排列其子元件、繪製背景、邊框、陰影與漸層,並處理按壓。按鈕、卡片、列與畫面都是 view。Rust 使用 view() 建立,TSX 則使用來自 nsh 的 <View>。

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))),
                ),
        )
}

#版面配置

View 以 flexbox 排列其子元件,預設值如下:flexDirection: "column"、flexShrink: 0、alignContent: "flex-start"、position: "relative"。display: "grid"(Rust 中為 .grid())會切換為 CSS grid,display: "block" 則切換為區塊版面。尺寸單位為 point。請參閱 layout 與 style reference。

display: "flex" 遵循 CSS:在 style 中、作為 flex class,以及 Rust 的 .flex() 時,除非設定了方向(flexDirection: "column"、flex-col、.flex_col()),否則為橫列。未設定 display 的 view 會將其子元件以直欄方式堆疊。

#Props

View 有一個專屬的 prop:selectable(Rust 中為 .selectable(true)),讓 view 內的文字作為單一選取範圍一起被選取(Text selection)。其餘為通用 props:樣式(style、class、classList;Rust 中為 Styled 方法)、disabled、按壓 props(onPress、onLongPress、onPressDown、onPressUp、delayLongPress、hitSlop)、狀態樣式(activeStyle、hoverStyle、focusStyle、disabledStyle、checkedStyle)、onMouseEnter / onMouseMove / onMouseLeave、onContextMenu、onFileDragEnter / onFileDragLeave / onFileDrop、focusable、onLayout、ref、nativeID、testID、手勢 props(pan、pinch、rotation 及其事件)、觸控與 responder props、無障礙 props,以及 ios / android / desktop。

#可選取的 View

設定 selectable 後,選取範圍會依閱讀順序跨越 view 內的 Text 元素與 TextView;設定 selectable={false} 的文字不會被納入,而位於另一個可選取 view 內的可選取 view 會成為外層 view 的一部分。各平台上的手勢請參閱 Text selection。

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

#按壓回饋

可按壓的 view 是指任何具有按壓處理函式的 view。其回饋由應用程式定義:按壓期間套用的樣式,會在手指按下時於原生端直接套用,無需往返應用程式程式碼。hoverStyle、focusStyle 與 disabledStyle 對其他狀態的作用相同:

// 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))

在 Android 上,android={{ ripple: { color: "#0000001f" } }}(Rust 中為 .ripple(Ripple { .. }))會加入 Material 漣漪效果(可用 borderless、radius 與 foreground 調整)。

disabled 會從原生 view 移除按壓監聽器(無法按壓、無回饋),並套用停用樣式:

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

#事件

事件RustPayload時機
onPress.on_pressPressEvent(Rust 中無)點按在 view 內結束。
onLongPress.on_long_pressPressEvent(Rust 中無)手指持續按住 delayLongPress(500 ms)。
onPressDown / onPressUp.on_press_down / .on_press_upPressEvent(Rust 中無)手指按下 / 抬起或移出。
onMouseEnter / onMouseLeave.on_mouse_enter / .on_mouse_leaveMouseEvent滑鼠移入 view(或其內部的 view)/ 離開 view 或視窗(hover);onMouseLeave 的值為滑鼠最後所在的位置。
onMouseMove.on_mouse_moveMouseEvent滑鼠在 view(或其內部的 view)上移動(無論是否按住按鍵),或滑鼠停留其上且未按住任何按鍵時,修飾鍵被按下或放開:position、pagePosition(Point)、modifiers 與 pressedButton("left"、"right"、"middle"、"back"、"forward" 或 null);每個影格最多觸發一次。
onContextMenu.on_context_menuContextMenuEvent在 view 上按下次要按鍵(context menus)。
onFileDragEnter / onFileDragLeave.on_file_drag_enter / .on_file_drag_leave無本機檔案拖曳進入 / 離開作用中的桌面目的地。
onFileDrop.on_file_drop{ uris: string[] }(FileDropEvent)本機檔案或目錄被放到桌面目的地上。
onLayout.on_layout{ bounds }(Bounds<Points>)相對於父層的 frame 發生變化。
onPanStart / onPanEnd.on_pan_start / .on_pan_endPanEvent以 pan 綁定的拖曳開始 / 結束;系統取消時 cancelled 為 true。
onPinchStart / onPinchEnd.on_pinch_start / .on_pinch_end{ scale, velocity }以 pinch 綁定的捏合開始 / 結束。
onRotationStart / onRotationEnd.on_rotation_start / .on_rotation_end{ rotation, velocity }以 rotation 綁定的旋轉開始 / 結束。
onAccessibilityAction.on_accessibility_action{ actionName }自訂的無障礙動作已執行。

觸控與 responder 事件:請參閱通用 props。

#桌面檔案拖放

在 AppKit、GTK、Win32 與 GPUI 上,設定 onFileDrop 的 view 可接收從其他應用程式拖入的本機檔案與目錄。onFileDragEnter 與 onFileDragLeave 用於追蹤作用中的目的地;onFileDrop 會依原生檔案清單的順序接收 { uris: string[] }。每個項目都是絕對的 file: URL,其中的空格與非 ASCII 字元皆已跳脫。請解析該 URL 以取得檔案系統路徑,而不要手動移除前綴。

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")))

最接近且已啟用、並監聽 onFileDrop 的祖先 view 會接收放下的檔案。modal 根節點會限定此搜尋範圍。disabled 會移除檔案拖放監聽器;離開、取消、完成與目的地移除都會清除作用中狀態。接受時使用 Copy 操作與已掛載的原生狀態。是否讀取、開啟或傳輸所接收的檔案,由應用程式決定。

#Ref 方法

一般 view 的 ref 是 ViewRef:focus()、blur() 與 measure()。ref(Rust 中為 .element_ref(&handle))會提供其節點,例如可傳給 setAccessibilityFocus(nsh 的無障礙 API),或用來測量該節點(Rust 中為 handle.measure())。

#文字內的 View

放在文字內的 view 是行內附件:它位於基線上、隨文字一起換行,並且可獨立接收按壓。請參閱 text。

#平台注意事項

  • boxShadow 在所有平台上皆會繪製;android={{ elevation }} 則改為使用 Android 的 Material 陰影。在 Android 上,陰影需要背景色(背景透明的 view 不會產生陰影)。
  • View 上的 filter:iOS 支援 opacity()、最大為 1 的 brightness() 以及 drop-shadow()(圖片支援所有色彩濾鏡與 blur());Android 自 API 31 起支援全部,更低版本則僅支援色彩濾鏡。
  • backdropFilter: "blur(…)" 在 iOS 上是系統材質(近似效果),在 Android 上需要 API 31。
  • Android 會繪製 translate、scale 與 rotate 轉換;傾斜(skew()、skewX()、skewY())則適用於其他平台。
  • borderCurve: "continuous"(.border_curve(BorderCurve::Continuous))會繪製與邊緣平滑銜接的圓角(如同 iOS 應用程式圖示),兩個平台皆支援。
  • cursor 樣式會套用於有指標裝置的環境(Mac Catalyst、搭配指標裝置的 iPad、桌面平台)。