इस पेज का अनुवाद मशीन द्वारा किया गया है और इसमें त्रुटियाँ हो सकती हैं। मूल अंग्रेज़ी पढ़ें

एलिमेंट

NavStack

नेविगेशन स्टैक एक समय में एक पेज दिखाता है और उसके नीचे के पेजों को बनाए रखता है: iOS और Mac Catalyst पर UINavigationController, सिस्टम हेडर और swipe back के साथ, और Android पर टूलबार और predictive back वाला पेजों का स्टैक। डेस्कटॉप (macOS, Windows, Linux, GPUI) और वेब पर हर पेज अपने सबसे ऊपर ऐप का अपना हेडर दिखाता है, और बैक कीज़ तथा ब्राउज़र का बैक बटन पेज को pop करते हैं। NavStack स्टैक है और NavPage उसका एक पेज।

ui-navigation.tsx
ui-navigation.tsxLive
// Navigation: a stack of pages with header options, pushed and popped; in a
// wide window the inbox and the open message side by side (master–detail).
import { type JSX, mount, signal, Text, View } from "nsh";
import { createNavStack, NavPage, NavStack } from "nsh/base";
import { button, buttonText, focusRing, hoverFill, size, tint, ui, wide } from "./palette";

function Button(props: { title: string; variant?: "primary" | "secondary"; onPress: () => void }) {
  const kind = () => (props.variant !== "secondary" ? "primary" : "secondary");
  return (
    <View role="button" onPress={props.onPress} {...button(kind())}>
      <Text style={buttonText(kind())}>{props.title}</Text>
    </View>
  );
}

type Route = { name: "home" } | { name: "detail"; id: number };

const nav = createNavStack<Route>([{ name: "home" }]);

// The page's own header where NSH draws none (the web, the desktops); iOS and
// Android show the system's, from `title` and `headerRight`.
function Header(props: { title: string; right?: JSX.Element }) {
  return (
    <View style={{ backgroundColor: ui().page, borderBottomWidth: 1, borderBottomColor: ui().line }} class="h-12 flex-row items-center px-2">
      <View class="w-20 flex-row">
        <If when={nav.routes().length > 1}>
          <View role="button" accessibilityLabel="Back" onPress={() => nav.pop()} hoverStyle={hoverFill()} focusStyle={focusRing()} class="rounded-md px-2 py-1 active:opacity-60">
            <Text style={{ fontSize: 17, color: ui().accent }}>‹ Back</Text>
          </View>
        </If>
      </View>
      <Text style={{ numberOfLines: 1, fontSize: 17, fontWeight: "600", color: ui().text }} class="flex-1 text-center">
        {props.title}
      </Text>
      <View class="w-20 flex-row justify-end px-2">{props.right}</View>
    </View>
  );
}

// A page's content: a column as wide as a phone, centered in a wider window.
function Body(props: { children: JSX.Element }) {
  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 16, paddingTop: 24 }}>
      <View style={{ width: "100%", maxWidth: 480, alignSelf: "center", gap: 12 }}>{props.children}</View>
    </View>
  );
}

function Home() {
  const opened = signal(0);
  return (
    <NavPage title="Inbox" header={<Header title="Inbox" />}>
      <Body>
        <Text style={{ color: ui().muted }}>{`Opened ${opened()} messages`}</Text>
        <Button
          title="Open message 1"
          onPress={() => {
            opened.set(opened() + 1);
            nav.push({ name: "detail", id: 1 });
          }}
        />
        <Button title="Open message 2" variant="secondary" onPress={() => nav.push({ name: "detail", id: 2 })} />
      </Body>
    </NavPage>
  );
}

function Detail(props: { id: number }) {
  const starred = signal(false);
  const star = () => (
    <Text onPress={() => starred.set(!starred())} style={{ fontSize: 17, color: ui().accent }} hoverStyle={hoverFill()} focusStyle={focusRing()} activeStyle={{ opacity: 0.7 }}>
      {starred() ? "★" : "☆"}
    </Text>
  );
  return (
    <NavPage title={`Message ${props.id}`} headerRight={star()} header={<Header title={`Message ${props.id}`} right={star()} />}>
      <Body>
        <Text style={{ color: ui().text }}>{`The body of message ${props.id}.`}</Text>
        <Button title="Next message" onPress={() => nav.push({ name: "detail", id: props.id + 1 })} />
        <Button title="Back to the inbox" variant="secondary" onPress={() => nav.popToRoot()} />
      </Body>
    </NavPage>
  );
}

const messages = [1, 2, 3, 4, 5, 6];

/** A wide window: the inbox on the start side, the selected message beside it. */
function Inbox() {
  const selected = signal(1);
  const starred = signal<number[]>([]);
  const star = (id: number) => (
    <Text
      onPress={() => starred.set(starred().includes(id) ? starred().filter((s) => s !== id) : [...starred(), id])}
      style={{ fontSize: 17, color: ui().accent }}
      hoverStyle={hoverFill()}
      focusStyle={focusRing()}
      activeStyle={{ opacity: 0.7 }}
    >
      {starred().includes(id) ? "★" : "☆"}
    </Text>
  );
  return (
    <View style={{ flex: 1, flexDirection: "row", backgroundColor: ui().page }}>
      <View style={{ width: 320, borderRightWidth: 1, borderRightColor: ui().line }}>
        <Header title="Inbox" />
        <View style={{ padding: 8, gap: 2 }}>
          <Each of={messages}>
            {(id) => (
              <View
                role="button"
                onPress={() => selected.set(id())}
                hoverStyle={selected() === id() ? undefined : hoverFill()}
                style={{
                  height: size.row + 12,
                  paddingHorizontal: 12,
                  borderRadius: 8,
                  justifyContent: "center",
                  gap: 2,
                  backgroundColor: selected() === id() ? tint(ui().accent, 0.14) : "transparent",
                }}
                focusStyle={focusRing()}
                activeStyle={{ opacity: 0.7 }}
              >
                <View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
                  <Text style={{ flex: 1, fontSize: size.label, fontWeight: "600", color: ui().text }}>{`Message ${id()}`}</Text>
                  <Text style={{ fontSize: 12, color: ui().accent }}>{starred().includes(id()) ? "★" : ""}</Text>
                </View>
                <Text style={{ numberOfLines: 1, fontSize: 12, color: ui().muted }}>{`The body of message ${id()}.`}</Text>
              </View>
            )}
          </Each>
        </View>
      </View>
      <View style={{ flex: 1, minWidth: 0 }}>
        <Header title={`Message ${selected()}`} right={star(selected())} />
        <View style={{ padding: 24, gap: 12 }}>
          <Text style={{ color: ui().text }}>{`The body of message ${selected()}.`}</Text>
          <Text style={{ fontSize: 13, color: ui().muted }}>Click a message on the left; on a phone it opens as a page of a NavStack.</Text>
          <Button title="Next message" onPress={() => selected.set(selected() === messages.length ? 1 : selected() + 1)} />
        </View>
      </View>
    </View>
  );
}

function App() {
  return (
    <>
      {() =>
        wide() ? (
          <Inbox />
        ) : (
          <NavStack state={nav} style={{ flex: 1 }}>
            {(route) => (route.name === "home" ? <Home /> : <Detail id={route.id} />)}
          </NavStack>
        )
      }
    </>
  );
}

mount(App);

स्टैक नेविगेशन के व्यवहार (ट्रांज़िशन, बैक जेस्चर और बटन, और ढके होने के दौरान पेजों की स्टेट) का मालिक है; पेजों का कंटेंट और डेस्कटॉप का हेडर ऐप के अपने होते हैं।

use nsh_base::{button, nav_page, nav_stack};
use nsh_core::prelude::*;

/// A button with the app's look.
fn action(title: &'static str, on_press: impl Fn() + 'static) -> impl IntoElement {
    button()
        .on_press(on_press)
        .rounded_lg()
        .bg(rgb(0x2563eb))
        .px_4()
        .py_3()
        .child(text(title).text_color(white()).font_weight(FontWeight::SEMIBOLD))
}

/// The header on the desktops and the web, with a back button over the first page.
fn bar(title: impl Into<SharedString>, routes: Signal<Vec<u32>>) -> impl IntoElement {
    view()
        .flex_row()
        .items_center()
        .gap_3()
        .px_4()
        .py_3()
        .border_b_1()
        .border_color(rgb(0xe5e5e5))
        .child(when(
            move || !routes.with(Vec::is_empty),
            move || {
                action("Back", move || {
                    routes.update(|r| {
                        r.pop();
                    })
                })
            },
        ))
        .child(text(title.into()).text_lg().font_weight(FontWeight::SEMIBOLD))
}

fn app() -> impl IntoElement {
    // The messages pushed over the inbox, bottom to top.
    let routes = signal(Vec::<u32>::new());
    nav_stack()
        .flex_1()
        .child(
            nav_page()
                .title("Inbox")
                .header(bar("Inbox", routes))
                .child(view().flex_1().p_4().child(action("Open message 1", move || {
                    routes.update(|r| r.push(1))
                }))),
        )
        .child(each(
            move || routes.get(),
            |id: &u32| *id,
            move |id, depth| {
                let title: SharedString = format!("Message {}", id.get_untracked()).into();
                nav_page()
                    .title(title.clone())
                    .header(bar(title, routes))
                    // The user went back natively: this page and those above it are gone.
                    .on_dismiss(move || routes.update(|r| r.truncate(depth.get_untracked())))
                    .child(view().flex_1().p_4().child(text("The message.")))
            },
        ))
}

iOS और Android पर पेज title और बैक बटन के साथ सिस्टम हेडर दिखाता है; डेस्कटॉप और वेब पर यह header दिखाता है। जो ऐप केवल फ़ोन को लक्षित करता है वह header को छोड़ देता है; जो केवल डेस्कटॉप को लक्षित करता है वह सिस्टम हेडर के विकल्प छोड़ देता है। Navigation गाइड पूरे ऐप के नेविगेशन को समझाती है: स्टैक की स्टेट, डायलॉग, Android का बैक बटन और टैब।

#स्टैक कैसे काम करता है

  • स्टैक के पेज routes होते हैं: आपके चुने हुए मान, स्टैक के सबसे नीचे से सबसे ऊपर तक। एक route आमतौर पर पेज का नाम बताता है और उसके पैरामीटर रखता है।
  • TSX में routes उस स्टेट में रहते हैं जो createNavStack बनाता है। NavStack हर push के लिए अपने child function को एक बार कॉल करता है और नतीजे को पेज के रूप में दिखाता है। जब तक उसका route स्टैक में है, यह उस पेज को बनाए रखता है, इसलिए दूसरे पेजों के ढकने पर भी पेज की स्टेट (signals, स्क्रॉल पोज़िशन, इनपुट) बनी रहती है।
  • Rust में स्टैक के children उसके पेज होते हैं: nav_stack() में रखा गया nav_page() push होता है, और जो छोड़ दिया जाए वह pop हो जाता है। routes को एक signal में रखें और पेजों को route के key के साथ each(..) से रेंडर करें, ताकि नीचे के पेज अपनी स्टेट बनाए रखें।
  • हर push अपने आप में एक अलग पेज है, एक ही route मान को दो बार push करने पर भी।
  • जब उपयोगकर्ता किसी पेज को नेटिव तरीके से छोड़ता है (बैक बटन, swipe back, Android back, डेस्कटॉप की बैक कीज़, ब्राउज़र का बैक), तो वह पेज हट जाता है। TSX में स्टैक उसका route स्टेट से हटा देता है, इसलिए nav.routes() हमेशा वही दिखाता है जो उपयोगकर्ता देखता है। Rust में पेज on_dismiss रिपोर्ट करता है, जहाँ आप उसका route (और उसके ऊपर के routes) हटा देते हैं।
  • TSX में, जो child function <NavPage> के अलावा कुछ और लौटाता है, उसे डिफ़ॉल्ट विकल्पों वाला पेज मिलता है। उसका हेडर और अन्य विकल्प सेट करने के लिए <NavPage> लौटाएँ।

#createNavStack

function createNavStack<R>(routes: R[]): NavStackState<R>

यह ऐसे स्टैक की स्टेट बनाता है जो routes से शुरू होता है, नीचे से ऊपर। इसे एक बार बनाएँ, उदाहरण के लिए मॉड्यूल स्तर पर, और <NavStack state> को दें। नेविगेट करने के लिए कोई भी कोड इसे import कर सकता है।

सदस्यविवरण
routes: Accessor<readonly R[]>routes, नीचे से ऊपर। Reactive।
push(route: R): voidएक पेज push करता है।
pop(count?: number): voidcount पेज pop करता है (डिफ़ॉल्ट 1)। कम से कम एक पेज हमेशा बना रहता है।
popToRoot(): voidपहले पेज तक वापस pop करता है।
replace(route: R): voidसबसे ऊपर के पेज को बदलता है।
setRoutes(routes: R[]): voidपेजों को बदलता है; जो route बना रहता है (वही मान) उसका पेज और उसकी स्टेट बनी रहती है।
remove(route: R): voidroute का सबसे ऊपर वाला पेज हटाता है (=== से तुलना करके), चाहे वह स्टैक में कहीं भी हो।

Rust में यही ऑपरेशन आपके routes signal में बदलाव हैं: routes.update(|r| r.push(route)) push करता है, r.pop() pop करता है, r.truncate(1) उसमें रखे पहले पेज तक वापस pop करता है, और routes.set(..) पेजों को बदलता है।

routes.update(|r| r.push(7));
routes.update(|r| r.truncate(r.len().saturating_sub(2)));
routes.set(vec![1, 2]);
let depth = move || routes.with(Vec::len);
PropRustप्रकारविवरण
stateपेज इसके children हैंNavStackState<R>वह स्टेट जो routes रखती है (createNavStack)। अनिवार्य।
children.child(..)(route: R, index: Accessor<number>) => JSX.Elementएक route को रेंडर करता है। उसके विकल्पों के लिए <NavPage> लौटाएँ। अनिवार्य।
style, classstyle methodsस्टैक की स्टाइल; आमतौर पर flex: 1।
testID.test_id(..)stringटेस्ट और nsh test --trace में स्टैक को खोजता है।

TSX का child function route का मान स्वयं और स्टैक में उसकी स्थिति का एक accessor प्राप्त करता है।

NavPage स्टैक का एक पेज है। यह पेज का कंटेंट, डेस्कटॉप और वेब पर उसका header, और iOS तथा Android पर सिस्टम हेडर के विकल्प रखता है। Rust में nav_page() पेज लौटाता है और उसके builder मेथड props के snake-case नाम होते हैं।

PropRustप्रकारडिफ़ॉल्टविवरण
header.header(..)JSX.Elementकोई नहींडेस्कटॉप और वेब: पेज का हेडर, जो पेज के सबसे ऊपर रखा जाता है, पेज जितना चौड़ा और अपने लेआउट जितना ऊँचा, और कंटेंट उसके नीचे।
title.title(..)stringकोई नहींiOS और Android: सिस्टम हेडर का शीर्षक।
headerVisible.header_visible(..)booleantrueiOS और Android: सिस्टम हेडर दिखता है।
headerTransparent.header_transparent(..)booleanfalseiOS और Android: कंटेंट पारदर्शी सिस्टम हेडर के नीचे तक फैलता है।
hidesBottomBarWhenPushed.hides_bottom_bar_when_pushed(..)booleanfalseiOS: जब तक यह पेज सबसे ऊपर है, सिस्टम टैब बार हट जाता है और ट्रांज़िशन में चलता है — पेज के जैसा ही एनिमेशन, और बार कभी दोबारा नहीं बनाया जाता; पेज के pop होने पर वह वापस आ जाता है।
headerLargeTitle.header_large_title(..)booleanfalseiOS: एक बड़ा शीर्षक जो कंटेंट के स्क्रॉल होने पर बार में सिकुड़ जाता है; कंटेंट हेडर के नीचे स्क्रॉल होता है।
headerBackTitle.header_back_title(..)stringकोई नहींiOS और Android: उस बैक बटन का लेबल जो इस पेज पर वापस लाता है (Android पर, स्क्रीन रीडर के लिए उसका विवरण)।
headerBackVisible.header_back_visible(..)booleantrueiOS और Android: बैक बटन दिखता है।
headerLeft.header_left(..)JSX.Elementकोई नहींiOS और Android: सिस्टम हेडर के आगे वाले किनारे पर कंटेंट, बैक बटन की जगह।
headerRight.header_right(..)JSX.Elementकोई नहींiOS और Android: सिस्टम हेडर के पीछे वाले किनारे पर कंटेंट।
headerTitle.header_title(..)JSX.Elementकोई नहींiOS और Android: वह कंटेंट जो शीर्षक की जगह लेता है।
headerStyle.header_style(..){ backgroundColor?, color?, tintColor? }प्लेटफ़ॉर्म का डिफ़ॉल्टiOS और Android: सिस्टम हेडर के रंग: उसकी पृष्ठभूमि, शीर्षक (color) और बटनों (tintColor) के रंग।
headerClass.header_style_with(|s| ..)stringकोई नहींवही रंग classes के रूप में: bg-*, शीर्षक के लिए text-*, बटनों के लिए tint-* (Rust में .bg(..), .text_color(..), .tint_color(..), थीम के रंग जो appearance का पालन करते हैं)। headerStyle इन पर प्रभावी रहता है।
popOnBack.pop_on_back(..)booleantrueबैक बटन और swipe back पेज को pop करते हैं। देखें किसी पेज को छोड़ना।
popOnSwipe.pop_on_swipe(..)booleantrueइंटरैक्टिव बैक जेस्चर पेज को pop करता है: iOS का swipe back, Android का predictive back जो उंगली का अनुसरण करता है। बैक बटन दोनों स्थितियों में pop करते हैं।
animation.animation(..)"default" | "slide" | "fade" | "none""default"पेज कैसे आता और जाता है।
transitionShader.transition_shader(TransitionShader::new(program)){ source: string; uniforms?: Record<string, ShaderUniform>; duration?: number }कोई नहींपेज अपने animation की जगह WGSL शेडर के ज़रिए आता और जाता है: shade(uv) नीचे के पेज को sample_from के रूप में और इस पेज को sample_to के रूप में पढ़ता है, जबकि nsh.progress duration मिलीसेकंड (डिफ़ॉल्ट 350) में 0 से 1 तक चलता है, और पेज के जाते समय 1 से वापस 0 तक। जब सिस्टम कम मोशन माँगता है, तो पेज अपना animation अपनाता है। देखें Shader।
statusBar.status_bar(..){ foreground?, hidden?, animated? }कोई नहींiOS और Android: जब तक पेज सबसे ऊपर है, तब तक स्टेटस बार। देखें स्टेटस बार।
style, classstyle methodsकोई नहींपेज की स्टाइल।
children.child(..)JSX.Elementकोई नहींकंटेंट।
testID.test_id(..)stringकोई नहींटेस्ट और nsh test --trace में पेज को खोजता है।
EventRustविवरण
onBackPress.on_back_press(..)उपयोगकर्ता ने पेज पर बैक दबाया (हेडर का बैक बटन, Android back, डेस्कटॉप पर Escape, Alt+Left या माउस का बैक बटन, ब्राउज़र का बैक), चाहे popOnBack ने उसे pop होने दिया हो या नहीं।
onDismiss.on_dismiss(..)उपयोगकर्ता ने पेज को pop किया (बैक बटन, swipe); TSX में उसका route हट चुका होता है, और Rust में आप उसे यहाँ हटाते हैं।

#हेडर स्लॉट

header, headerLeft, headerRight और headerTitle स्लॉट हैं: साधारण एलिमेंट जो signals पढ़ते हैं, प्रेस को हैंडल करते हैं और पेज के दिखने के दौरान अपडेट होते हैं। डेस्कटॉप और वेब पर header ऐप का पूरा हेडर है; बाकी तीन iOS और Android पर सिस्टम हेडर में जाते हैं।

use nsh_base::nav_page;
use nsh_core::prelude::*;

fn profile(edit: impl Fn() + 'static) -> Screen {
    nav_page()
        .title("Profile")
        .header(profile_bar())
        .header_style_with(|s| s.bg(rgb(0x1a7f37)).text_color(white()).tint_color(white()))
        .header_right(text("Edit").text_color(white()).on_press(move || edit()))
        .child(scroll().flex_1())
}

पेज का कंटेंट हेडर के नीचे से शुरू होता है: iOS और Android पर अपारदर्शी सिस्टम हेडर के नीचे, डेस्कटॉप और वेब पर header के नीचे। headerTransparent, headerLargeTitle या headerVisible={false} के साथ, यह iOS और Android पर पेज के सबसे ऊपर से शुरू होता है; इसे हेडर और स्टेटस बार से दूर रखने के लिए env(safe-area-inset-top) या safeAreaInsets() का उपयोग करें।

iOS पर, headerVisible={false} तब तक swipe back को चालू रखता है जब तक popOnSwipe और popOnBack true हैं। स्वीकार किया गया इंटरैक्टिव बैक जेस्चर ट्रांज़िशन शुरू होते ही एडिटिंग समाप्त कर देता है, इसलिए ड्रैग के दौरान कीबोर्ड बंद हो जाता है। ट्रांज़िशन रद्द करने पर, अगर किसी दूसरे कंट्रोल ने फ़ोकस नहीं लिया है, तो उसी एडिटर का फ़ोकस बहाल हो जाता है।

nav_page()
    .title("Large title")
    .header_large_title(true)
    .child(scroll().flex_1())

#किसी पेज को छोड़ना

popOnBack={false} उपयोगकर्ता को पेज पर रोके रखता है, उदाहरण के लिए जब किसी फ़ॉर्म में बिना सहेजे बदलाव हों। जब यह false होता है, swipe back बंद रहता है और बैक बटन तथा कीज़ pop किए बिना onBackPress को कॉल करते हैं। उपयोगकर्ता से पूछें, फिर खुद pop करें:

use nsh_base::{alert_dialog, nav_page};
use nsh_core::prelude::*;

fn edit_note(routes: Signal<Vec<u32>>) -> Screen {
    let note = signal(String::new());
    let confirm = move || {
        spawn(async move {
            let choice = alert_dialog()
                .title("Discard your changes?")
                .cancel("keep", "Keep editing")
                .destructive("discard", "Discard")
                .show()
                .await;
            if choice.is_ok_and(|r| r.action.as_deref() == Some("discard")) {
                routes.update(|r| {
                    r.pop();
                });
            }
        });
    };
    nav_page()
        .title("Edit")
        .pop_on_back(move || note.with(String::is_empty))
        .on_back_press(move || {
            if !note.with(String::is_empty) {
                confirm();
            }
        })
        .child(
            input()
                .value(note)
                .on_change(move |v: &str| note.set(v.to_owned()))
                .placeholder("A note"),
        )
}

onBackPress हर बैक प्रेस पर कॉल होता है, पेज के pop होने पर भी, इसलिए यह आपको यह भी बताता है कि उपयोगकर्ता वापस गया। केवल swipe जेस्चर बंद करने और बैक बटन चालू रखने के लिए popOnSwipe={false} का उपयोग करें। पुष्टि वाला डायलॉग स्वयं एक AlertDialog है।

#प्लेटफ़ॉर्म नोट्स

  • iOS: सिस्टम हेडर, बैक बटन और swipe back के साथ UINavigationController; headerLargeTitle UIKit का बड़ा शीर्षक है। चल रहे ट्रांज़िशन के दौरान आने वाले स्टैक बदलाव उसके समाप्त होने पर लागू होते हैं।
  • Android: टूलबार वाला स्टैक। सिस्टम का बैक बटन सबसे ऊपर के पेज को pop करता है, और जिन Android वर्ज़न में predictive back है उन पर उसके साथ; स्क्रीन रीडर के लिए अप एरो का विवरण नीचे वाले पेज का headerBackTitle होता है, या सिस्टम का। बैक बटन को खुद हैंडल करने के लिए BackHandler में BackHandler देखें।
  • Mac Catalyst: iOS की तरह।
  • डेस्कटॉप (macOS, Windows, Linux, GPUI) और वेब: हर पेज अपने सबसे ऊपर अपना header और उसके नीचे अपना कंटेंट दिखाता है। डेस्कटॉप पर Escape, Alt+Left और माउस का बैक बटन वापस ले जाते हैं, और वेब पर ब्राउज़र का बैक बटन।

स्टैक और उसके हेडर के ऊपर दिखाए जाने वाले कंटेंट के लिए, जैसे sheet या फ़ुल-स्क्रीन फ़्लो, एक Dialog का उपयोग करें।