이 페이지의 번역은 오류가 있을 수 있습니다. 영어 원문 보기

요소

요소

Native Shell의 모든 화면은 View, Text, Image 같은 기본 요소와 Button, Dialog, NavStack 같은 기본 컴포넌트로 구성됩니다. 각 기본 요소는 NSH 런타임이 생성하고 배치하는 네이티브 뷰입니다. iOS와 Mac Catalyst에서는 UIView의 하위 클래스, Android에서는 View, 데스크톱에서는 AppKit, GTK 또는 Win32 뷰를 사용합니다. 앱에서 사용하는 버튼, 카드, 행, 화면은 이 요소들로 만든 컴포넌트이며, 각각의 스타일과 앱 전체의 디자인은 앱에서 결정합니다.

예제는 Rust와 TSX로 제공합니다. 탭에서 언어를 고르면 같은 페이지에 있는 다른 예제에도 적용됩니다.

use nsh_core::prelude::*;

fn app() -> impl IntoElement {
    let count = signal(0_u32);
    view()
        .flex_1()
        .items_center()
        .justify_center()
        .gap_3()
        .child(text(move || format!("Tapped {} times", count.get())).text_2xl())
        .child(
            view()
                .rounded_lg()
                .bg(rgb(0x2563eb))
                .px_4()
                .py_2()
                .active(|s| s.opacity(0.6))
                .on_press(move || count.update(|n| *n += 1))
                .child(text("Tap").text_color(white())),
        )
}

Rust의 기본 요소는 nsh_core::prelude의 함수(view(), text(..), image(..))이며, 기본 컴포넌트는 nsh_base 크레이트에서 가져옵니다. 스타일은 타입이 있는 메서드(.flex_1(), .gap_3(), .px(px(20.0)), .bg(..))로 지정하고, 상태별 스타일에는 클로저(.active(|s| s.opacity(0.6)))를 전달합니다. 이벤트 리스너도 클로저(.on_press(move || ..), .on_change(move |text: &str| ..))이며, 자식은 .child(..)로 추가합니다. UI를 구성하는 일반 함수는 한 번 실행되어 impl IntoElement를 반환합니다. text(move || ..), .bg(move || ..) 등의 클로저 안에서 읽은 시그널이 바뀌면 해당 바인딩이 갱신됩니다. Rust로 앱 작성하기를 참고하세요.

TSX 태그는 PascalCase로 작성하며 현재 범위에 있는 이름을 참조합니다. 기본 요소는 nsh에서, 기본 컴포넌트는 nsh/base에서 가져옵니다. 전역인 <If>와 <Each>만 가져오기 없이 쓸 수 있습니다. <view>처럼 소문자로 작성하면 빌드 오류가 발생합니다. 소문자 태그를 허용하면 TypeScript가 태그 이름마다 하나의 전역 타입을 사용하게 되어 List와 Each 같은 제네릭 컴포넌트에서 항목 타입을 추론할 수 없기 때문입니다. 컴파일러는 가져온 요소를 식별하고 사용 위치에 네이티브 뷰를 생성하므로 가져오기에 따른 런타임 비용은 없습니다. JSX와 컴포넌트에서 더 알아볼 수 있습니다.

import { Canvas, Image, List, Overlay, Scroll, Shader, Text, Video, View, WebView } from "nsh";
import { Button, Dialog, Menu, NavPage, NavStack, Popover, Sheet, Tooltip, Input } from "nsh/base";

#기본 요소

TSX에서는 nsh에서, Rust에서는 nsh_core::prelude에서 가져옵니다.

요소Rust네이티브 뷰(iOS / Android)문서
<View>view()UIView / ViewGroup. selectable이면 내부 텍스트를 함께 선택할 수 있음View, 텍스트 선택
<Text>text(content)CoreText 뷰 / StaticLayout 뷰Text
<Image>image(src)이미지 레이어가 있는 뷰 / 이미지 뷰Image
<Scroll>scroll()UIScrollView / ScrollViewScroll
<List>virtual_list(count, |at| row)셀을 재사용하는 UIScrollView / ScrollViewList
<Canvas>canvas(painter)2D 그리기 명령을 재생하는 뷰Canvas
<Shader>shader(program)자식 뷰 아래에 WGSL 프로그램으로 영역을 그리는 GPU 뷰Shader
<WebView>web_view(url), web_view_html(html, base)WKWebView / android.webkit.WebViewWebView
<Video>video(url)AVPlayerLayer / TextureView의 MediaPlayerVideo
<Overlay>overlay()창 또는 자신을 포함하는 대화상자 맨 위에 앵커를 기준으로 배치되는 뷰Overlay
<If>, <Each>when(..), each(..), dynamic(..)제어 흐름에 따라 선택되는 요소제어 흐름
<InputAccessoryView> (nsh/mobile)input_accessory_view(id)iOS 키보드 위에 표시되는 막대InputAccessoryView

패딩과 마진에 CSS의 env()를 사용하면 시스템 UI가 콘텐츠를 가리지 않도록 여백을 확보할 수 있습니다. 가장자리의 안전 영역에는 env(safe-area-inset-top) 등 방향별 변수를, 키보드에는 env(keyboard-inset-height)를 사용합니다.

#기본 컴포넌트

TSX에서는 nsh/base에서, Rust에서는 nsh_base 크레이트에서 가져옵니다. 각 컴포넌트가 동작, 상태, 포커스, 접근성을 처리하고 앱에서 모양을 지정합니다.

컴포넌트Rust기능문서
AlertDialognsh_base::alert_dialog()시스템 알림 대화상자와 액션 시트AlertDialog
<Accordion> 및 하위 요소nsh_base::accordion(), accordion_item(), …제목을 눌러 하나 또는 여러 섹션을 펼치고 접을 수 있는 목록Accordion
<Avatar>, <AvatarGroup>nsh_base::avatar(), avatar_group()프로필 사진과 로딩 중이거나 실패했을 때 표시할 대체 콘텐츠. 그룹은 앞에서부터 정해진 수만큼 표시Avatar
<Button>nsh_base::button()상태와 접근성 역할을 갖춘 누를 수 있는 컨트롤Button
<Carousel> 및 하위 요소nsh_base::carousel(), carousel_content(), …네이티브 스와이프, 이전·다음, 페이지 표시, 키보드, 끊김 없는 순환을 지원하며 슬라이드를 한 위치에 표시Carousel
<Checkbox>nsh_base::checkbox()선택, 해제, 부분 선택 상태를 표시하는 컨트롤Checkbox
<Collapsible>nsh_base::collapsible()내용을 펼치거나 접을 수 있는 영역Collapsible
<Command>nsh_base::command()검색 필드, 네이티브 필터링, 가상화된 목록과 Cmd+K 방식의 키 조작을 제공하는 명령 팔레트Command
<DataTable>nsh_base::data_table()행·열 수에 관계없이 선택과 정렬, 열 크기 조절과 이동을 지원하는 표DataTable
<Dialog>nsh_base::dialog()자식 콘텐츠를 다른 모든 콘텐츠 위에 표시Dialog
<DiffView>nsh_base::diff_view()두 텍스트의 차이나 패치를 분할 또는 통합 보기로 표시DiffView
<DockArea>nsh_base::dock_area()사용자가 분할 영역, 탭 그룹, 가장자리 도크를 배치하는 작업 공간Dock
<Editor>nsh_base::editor()구문 강조, 다중 커서, 코드 접기, 검색을 지원하는 편집기Editor
<HoverCard>nsh_base::hover_card()포인터가 트리거 위에 머무는 동안 대상 콘텐츠를 미리 표시HoverCard
<Input>nsh_base::input()네이티브 텍스트 입력 필드(UITextField 또는 UITextView / EditText)Input
<Link>nsh_base::link()누르면 페이지, 웹 사이트 또는 앱 내부 경로를 여는 요소Link
<Menu>, <ContextMenu>, <MenuItem>, <MenuSeparator>, <Submenu>nsh_base::menu(), …데스크톱에서는 하위 메뉴가 이어지는 팝업, 휴대폰에서는 시트로 표시하는 메뉴Menu
<NavStack>, <NavPage>nsh_base::nav_stack(), nsh_base::nav_page()플랫폼 고유의 헤더와 뒤로 가기 제스처를 사용하는 페이지 스택NavStack
<NumberInput>nsh_base::number_input()입력란에 직접 입력하거나 버튼과 키로 증감하는 숫자NumberInput
<OtpInput>nsh_base::otp_input()고정 자릿수의 일회용 코드를 셀에 입력하거나 붙여넣고 자동 완성OtpInput
<Pagination>nsh_base::pagination()이전·다음, 현재 페이지 주변 번호와 생략 기호로 구성된 페이지 탐색Pagination
<Tree>nsh_base::tree()하위 항목을 펼치고 접으며 항목 하나를 선택하는 가상화 트리. Tab 키로는 하나의 포커스 지점으로 이동Tree
<Plot>, <Line>, <Area>, <Bar>, <Grid>, <Pie>, <Arc> (nsh/base/plot)nsh_base::plot::{plot, line, area, bar, grid, pie, arc}앱 데이터의 각 행을 바탕으로 네이티브에서 그리는 차트 영역과 도형Plot
<Popover>nsh_base::popover()트리거 위치를 기준으로 다른 콘텐츠 위에 표시하는 팝업Popover
<Progress>nsh_base::progress()작업 진행률 또는 작업이 진행 중임을 표시하는 막대Progress
<Radio>, <RadioGroup>nsh_base::radio(), radio_group()여러 선택지 중 하나를 고르는 항목과 그룹Radio
<ResizablePanelGroup>, <ResizablePanel>nsh_base::h_resizable(), resizable_panel()가로나 세로로 배치하며 핸들로 크기를 바꾸는 패널Resizable
<Scrollbar>nsh_base::scrollbar()스크롤 뷰나 목록에 표시하는 앱 디자인의 스크롤 막대Scrollbar
<Select>nsh_base::select()검색, 키보드 탐색과 섹션을 지원하는 목록에서 값 하나를 선택Select
<Sheet>nsh_base::sheet()배경을 덮는 레이어 위에 화면 가장자리를 따라 표시되는 패널Sheet
<Slider>nsh_base::slider()트랙 위에서 하나의 값이나 두 값으로 정한 범위를 선택Slider
<Switch>nsh_base::switch()트랙과 손잡이로 켜짐과 꺼짐을 표시하는 컨트롤Switch
<Table> 및 하위 요소nsh_base::table(), table_row(), …표에 필요한 역할을 갖추고 앱의 행과 셀로 구성되는 표Table
<Tab>, <Tabs>nsh_base::tab(), tabs()탭 목록과 목록 안의 개별 탭Tabs
<TextView>nsh_base::text_view()텍스트와 뷰로 Markdown 또는 HTML 문서를 표시TextView
<Toast>, <ToastStack>nsh_base::toast(), toast_stack()토스트 하나의 의미 구조 및 화면 가장자리에 여러 토스트를 겹쳐 배치하는 기능Toast
<Toggle>, <ToggleGroup>nsh_base::toggle(), toggle_group()켜거나 끌 수 있는 버튼과 그러한 버튼의 도구 모음Toggle
<Toolbar>, <ToolbarGroup>, <ToolbarSeparator>nsh_base::toolbar(), toolbar_group(), toolbar_separator()버튼, 토글, 링크로 구성되며 키보드 포커스가 한 번 멈추는 도구 모음Toolbar
<Tooltip>nsh_base::tooltip()포인터를 올리거나 길게 누르면 트리거 옆에 표시되는 설명Tooltip

#공통 속성

모든 기본 요소는 다음 속성을 받습니다. TSX에서는 CommonProps 타입에, Rust에서는 Styled, Interactive, Accessible 트레이트에 정의되어 있습니다. 각 요소의 개별 문서에서는 그 요소만의 속성을 설명합니다. 표의 Rust 열에는 빌더 메서드를 표시했습니다.

#스타일 지정

속성Rust타입설명
styletyped Styled methods; .inline_style(style)StyleProp<ViewStyle> (TextStyle for <Text> and <Input>, ImageStyle for <Image>)인라인 스타일 또는 왼쪽부터 순서대로 병합할 스타일 배열. 거짓으로 평가되는 항목은 건너뜁니다. 배열을 중첩할 수 있어 style={[base, props.style]}에서 props.style도 배열일 수 있습니다. class보다 우선합니다. 스타일 레퍼런스를 참고하세요.
classtyped Styled methods (.flex_row().gap_2().p_4())string네이티브에서 해석하는 Tailwind 클래스("flex-row gap-2 p-4 dark:bg-zinc-900"). 뒤에 오는 클래스가 우선합니다. Tailwind를 참고하세요.
classList.style_when(when, |s| ..)Record<string, boolean | null | undefined>값이 참으로 평가되는 동안 적용할 클래스({ "opacity-50": busy() }).
disabled.disabled(..)boolean누르기와 시각적 피드백을 무시하고 disabled: 스타일을 적용합니다. 입력 필드 등의 컨트롤을 비활성화하며 스크린 리더에도 비활성 상태(accessibilityState.disabled)를 알립니다.

#누르기와 피드백

누르기는 네이티브에서 처리합니다. 플랫폼이 누르기를 인식하고 앱이 지정한 스타일을 적용한 뒤 앱 코드에 이벤트를 전달합니다. 누를 수 있는 뷰가 중첩되어 있으면 가장 안쪽 뷰만 반응합니다. 스크롤 뷰 안에서는 스크롤이 먼저 시작될 수 있도록 누름 시작 처리를 약 130ms 늦춥니다.

속성Rust타입기본값설명
onPress.on_press(..)(e: PressEvent) => void뷰 안에서 탭을 마쳤을 때.
onLongPress.on_long_press(..)(e: PressEvent) => void손가락을 delayLongPress 동안 누르고 있을 때.
onPressDown.on_press_down(..)(e: PressEvent) => void손가락이 뷰에 닿았을 때.
onPressUp.on_press_up(..)(e: PressEvent) => void손가락을 떼거나 뷰를 벗어났을 때.
delayLongPress.long_press_delay(ms)number500길게 누르기로 인식하기까지의 시간(ms).
hitSlop.hit_slop(EdgeInsets::all(px(8.0)))number | Partial<Edges>뷰 경계 밖으로 터치 영역을 확장하는 거리(포인트).

PressEvent는 { position, pagePosition, modifiers, clickCount }로 구성됩니다. 차례대로 뷰와 루트 뷰 기준 포인터 위치(Point), 누르기 시작 시점에 눌린 보조 키, 연속 클릭 수(더블 클릭은 2)입니다. 플랫폼이 포인터 위치를 제공하지 않으면 뷰의 원점, 보조 키 없음, 클릭 1회로 전달됩니다.

스타일 속성 pointerEvents(Rust에서는 .pointer_events(PointerEvents::BoxNone), 클래스는 pointer-events-*)는 뷰와 자식 뷰 중 어디에서 터치를 받을지 결정합니다. "none"이면 둘 다 받지 않지만 하위 뷰가 "auto"를 지정하면 그 뷰는 다시 받습니다. "box-none"은 자식만, "box-only"는 뷰 자신만 받습니다. 터치와 제스처를 참고하세요.

눌렀을 때의 모양은 앱이 정합니다. 뷰를 누르고 있는 동안 플랫폼이 activeStyle(또는 active: 클래스, Rust의 .active(..))을 직접 적용하므로 앱 코드를 거칠 필요가 없습니다.

#상태별 스타일

뷰가 특정 상태일 때 네이티브에서 해당 스타일을 뷰 자체의 스타일 위에 적용합니다. 각 속성은 Tailwind 변형을 객체로 표현한 것입니다. selected, checked·indeterminate, hover, focus, active 순으로 뒤에 오는 상태가 우선합니다. 비활성화된 동안에는 disabledStyle이 가장 우선하며 다른 상태 스타일을 적용하지 않습니다. 속성값은 요소에 맞는 스타일 타입을 사용합니다. <Text>는 TextStyle, <Input>은 InputStyle, <Image>는 ImageStyle, 그 밖에는 ViewStyle입니다.

속성Rust클래스 변형적용 조건
activeStyle.active(|s| ..)active:누르고 있는 동안.
hoverStyle.hover(|s| ..)hover:포인터가 뷰 위에 있는 동안.
focusStyle.focus(|s| ..)focus:뷰에 포커스가 있는 동안.
focusWithinStyle.focus_within(|s| ..)focus-within:뷰나 그 자식에 포커스가 있는 동안. Overlay의 콘텐츠도 해당 오버레이를 포함하는 뷰 내부로 간주합니다.
disabledStyle.disabled_style(|s| ..)disabled:disabled인 동안.
checkedStyle.checked(|s| ..), .checked_style(style)checked:accessibilityState.checked가 true일 때(체크박스, 스위치, 라디오). "mixed"는 제외합니다.
indeterminateStyle.indeterminate(|s| ..), .indeterminate_style(style)indeterminate:accessibilityState.checked가 "mixed"일 때(부분 선택된 체크박스).
selectedStyle.selected(|s| ..)selected:accessibilityState.selected가 true일 때(선택된 Button이나 탭).
expandedStyle.expanded(|s| ..), .expanded_style(style)aria-expanded:accessibilityState.expanded가 true일 때(팝업이 열려 있는 Popover나 Menu의 트리거).
pressedStyle.aria_pressed(|s| ..), .pressed_style(style)aria-pressed:accessibilityState.pressed가 true일 때(켜진 토글 버튼). "mixed"는 제외합니다. 실제로 누르고 있는 동안의 모양은 activeStyle로 지정합니다.
view()
    .rounded_lg()
    .bg(rgb(0x2563eb))
    .px_4()
    .py_2()
    .active(|s| s.opacity(0.6))
    .hover(|s| s.bg(rgb(0x1d4ed8)))
    .on_press(save)
    .child(text("Save").text_color(white()))

#호버

앱은 포인터가 뷰에 들어오고, 그 위를 움직이고, 벗어났을 때 이벤트를 받습니다. macOS, Windows, Linux, GPUI 백엔드, 데스크톱 브라우저뿐 아니라 iPad의 포인터나 Mac의 마우스를 사용하는 iOS 환경, 마우스나 스타일러스를 사용하는 Android 환경에서도 지원합니다. 같은 환경에서 hover:, .hover(..)로 포인터가 올라간 뷰의 스타일을 지정할 수 있습니다.

속성Rust타입설명
onMouseEnter.on_mouse_enter(..)(e: MouseEvent) => void마우스가 뷰 또는 그 안의 뷰로 들어왔습니다(DOM의 mouseenter).
onMouseMove.on_mouse_move(..)(e: MouseEvent) => void버튼을 누른 상태에 상관없이 마우스가 뷰 또는 그 안의 뷰 위에서 움직였습니다. 버튼을 누르지 않고 머무르는 동안 보조 키를 누르거나 떼도 발생합니다(DOM의 mousemove).
onMouseLeave.on_mouse_leave(..)(e: MouseEvent) => void마우스가 뷰와 그 안의 모든 뷰를 벗어났거나 창을 벗어났습니다. e에는 마지막 위치가 담깁니다(DOM의 mouseleave).

CSS의 :hover나 DOM의 mouseenter와 마찬가지로 마우스가 뷰 자체 또는 그 안의 요소 위에 있으면 해당 뷰에 호버한 상태입니다. 따라서 포인터 바로 아래의 뷰뿐 아니라 이를 감싸는 뷰도 바깥쪽부터 안쪽 순으로 onMouseEnter를 받습니다. onMouseLeave는 반대 순서입니다. 두 이벤트는 상태가 바뀐 뒤에 전달되며 터치로 발생하지 않습니다. 마우스 아래 있던 뷰를 제거해도 onMouseLeave가 발생합니다. Linux와 Windows에서는 다음에 마우스가 움직일 때 전달됩니다.

MouseEvent는 GPUI의 MouseMoveEvent처럼 마우스 위치를 알려 줍니다. position은 뷰의 변환을 역으로 적용한 로컬 좌표(포인트 단위의 Point), pagePosition은 터치 이벤트와 같은 루트 뷰 기준 좌표입니다. modifiers에는 누르고 있는 보조 키(control, alt, shift, platform)가 담깁니다. platform은 Apple에서는 Command 키, 다른 플랫폼에서는 Windows 키입니다. pressedButton은 현재 누른 버튼("left", "right", "middle", "back", "forward", GPUI의 MouseButton) 또는 null입니다. Rust 필드명은 position, page_position, modifiers, pressed_button입니다. 데스크톱과 웹의 드래그는 터치(onTouchMove, 리스폰더)로도 처리되며 pressedButton이 설정된 onMouseMove도 발생합니다. 단순 호버 피드백을 그릴 때는 버튼을 누른 이동 이벤트를 건너뛰세요. iOS와 Android의 호버는 버튼을 누르지 않은 포인터를 보고합니다. 이동은 프레임당 최대 한 번, 마지막 위치만 전달하며 뒤따르는 onMouseLeave보다 먼저 전달하므로 이동할 때마다 핸들러에서 다시 그릴 수 있습니다.

// A crosshair over a chart.
let at = signal(None::<Points>);

view()
    .h(px(200.0))
    .on_mouse_move(move |e: &MouseEvent| at.set(Some(e.position.x)))
    .on_mouse_leave(move |_: &MouseEvent| at.set(None))
    .child(when(move || at.get().is_some(), move || {
        view()
            .absolute()
            .left(move || at.get().unwrap_or_default())
            .top_0()
            .bottom_0()
            .w(px(1.0))
            .bg(rgb(0x888888))
    }))

#툴팁

모든 요소에 tooltip으로 도움말을 지정할 수 있습니다. 데스크톱(macOS, Windows, Linux, GPUI 백엔드, 데스크톱 브라우저)에서는 포인터가 요소 위에 0.5초 머물면 위쪽에 나타나며, 포인터가 떠나거나 요소를 누르면 사라집니다. 휴대폰과 태블릿에서는 표시되지 않습니다. 툴팁은 포인터나 누르기 이벤트를 가로채지 않으므로 요소를 그대로 조작할 수 있습니다.

속성Rust타입설명
tooltip.tooltip(..)JSX.Element앱에서 스타일을 지정한 도움말 요소입니다. nsh/base의 Tooltip을 쓰면 배치, 지연 시간, 이벤트가 지정된 툴팁 자체가 됩니다. 다른 요소는 작성한 그대로 표시됩니다.
view()
    .on_press(save)
    .tooltip(
        tooltip()
            .rounded_md()
            .bg(rgb(0x111827))
            .px_2()
            .py_1()
            .child(text("Save the document").text_color(white())),
    )
    .child(text("💾"))

GPUI의 .tooltip(..)에 툴팁을 전달하듯 Tooltip 자체에서 모양, 위치, 지연 시간, 이벤트를 지정합니다. 툴팁을 여럿 쓸 때는 앱에서 텍스트를 Tooltip으로 감싸는 작은 컴포넌트를 만들어 재사용하세요.

#컨텍스트 메뉴

onContextMenu는 뷰 위에서 보조 버튼을 눌렀을 때 발생합니다. 오른쪽 클릭, macOS의 Control-클릭, 터치패드의 두 손가락 클릭, iPad 포인터의 보조 클릭, 브라우저에서 키보드의 메뉴 키를 누르는 경우가 해당합니다. 이 위치에 앱 자체 메뉴를 열 수 있고, TSX에서는 Menu.popup으로 시스템 메뉴를 열 수 있습니다.

// The app's own menu, placed where the click was.
let menu_at = signal(None::<Point<Points>>);

view()
    .on_context_menu(move |e: &ContextMenuEvent| menu_at.set(Some(e.page_position)))
    .child(text(row.name.clone()))
속성Rust타입설명
onContextMenu.on_context_menu(..)(e: ContextMenuEvent) => void뷰 또는 그 안의 뷰 위에서 보조 버튼을 눌렀습니다.

ContextMenuEvent에는 뷰 안의 좌표 position, 루트 뷰 기준 좌표 pagePosition, 보조 키 modifiers가 있습니다. Rust에서는 position, page_position, modifiers입니다. Menu.popup(items, e.pagePosition)은 루트 좌표에 메뉴를 표시합니다. 포인터 아래의 뷰에서 바깥쪽으로 올라가며 이벤트를 수신하는 가장 가까운 뷰가 이를 받습니다. 그보다 안쪽에 텍스트 입력란이 있거나 편집 메뉴를 가진 선택 가능 텍스트(Windows, GPUI 백엔드, 브라우저)가 있으면 자체 메뉴를 표시합니다. 플랫폼이 버튼을 누르는 시점에 현재 마운트된 뷰를 보고 결정하므로 핸들러에서는 결과를 통지받기만 하며 기본 동작을 취소할 필요가 없습니다. disabled 상태인 뷰는 가장 가까운 활성 상위 리스너에 클릭을 넘깁니다. 터치스크린에서는 길게 눌러 메뉴를 열므로 같은 함수를 onLongPress에도 연결하세요.

#키보드 포커스

macOS와 웹에서 Tab을 누르면 트리 순서에 따라 입력란, 누를 수 있는 뷰, focusable 뷰로 키보드 포커스가 이동합니다. 현재 뷰에는 focus: 또는 .focus(..) 스타일이 적용되고, 해당 뷰를 누를 수 있다면 Return이나 Space로 누릅니다. 마우스로 클릭하면 포커스를 옮기지 않고 누르기만 하므로 포커스 스타일은 키보드 포커스에만 나타납니다.

포커스와 누르기는 독립적으로 처리됩니다. 차트, 슬라이더, 방향키로 선택을 이동하는 목록처럼 포커스를 가졌을 때 키를 처리하는 뷰는 focusable을 설정하고 포커스를 유지하는 동안에만 키를 바인딩합니다(onFocus, onBlur와 keymap 사용). 그 뷰를 누르면 페이지를 여는 카드처럼 주변에서 누르기를 처리하는 뷰에 이벤트가 전달됩니다.

ref의 focus()는 Tab처럼 포커스를 이동하고 포커스 스타일과 onFocus를 활성화합니다. 예를 들어 팝업을 닫았을 때 원래 팝업을 연 요소에 포커스를 돌려줄 수 있습니다. blur()는 포커스를 해제합니다.

focus-within:(focusWithinStyle, .focus_within(..))은 뷰 자신이나 그 안의 뷰에 포커스가 있을 때 적용됩니다. 편집 중인 입력란의 테두리나 버튼에 포커스가 있는 도구 모음의 스타일을 지정할 때 쓸 수 있습니다. Overlay의 내용은 표시 위치에 상관없이 오버레이가 속한 뷰 안에 있는 것으로 처리됩니다. 따라서 팝업도 이를 여는 요소가 속한 뷰의 일부입니다. Rust 컴포넌트에서는 요소의 ref에서 on_focus_in(..), on_focus_out(..)으로 포커스가 경계를 넘나드는 시점을 감지합니다. 내부 뷰 사이에서만 이동할 때는 둘 다 발생하지 않습니다.

속성Rust타입기본값설명
focusable.focusable(..)boolean누르기를 처리하는 뷰는 trueTab으로 포커스를 받을 수 있는지 지정합니다. true이면 뷰가 직접 누르기를 처리하든 상위 뷰에 넘기든 Tab으로 접근할 수 있습니다. false는 팝오버를 닫는 배경이나 방향키로 이동하는 행처럼 누를 수 있는 뷰를 Tab 순서에서 제외합니다.
onFocus.on_focus(..)() => voidTab 또는 focus()로 뷰가 키보드 포커스를 받았습니다.
onBlur.on_blur(..)() => void키보드 포커스가 뷰를 떠났습니다.
view()
    .rounded_lg()
    .p_3()
    .focus(|s| s.bg(rgb(0xe0f2fe)))
    .on_press(open)
    .child(text("Open"))

// An overlay's backdrop: a click closes it, Tab goes past it to the content.
view().absolute().inset_0().focusable(false).on_press(close)

// A chart in a card: Tab stops at the chart, a click opens the card's page.
view().on_press(open_page).child(
    view()
        .h(px(160.0))
        .focusable(true)
        .focus(|s| s.bg(rgb(0xe0f2fe)))
        .on_focus(move || focused.set(true))
        .on_blur(move || focused.set(false)),
)

#레이아웃과 식별자

속성Rust타입설명
onLayout.on_layout(|frame: &Bounds<Points>| ..)(e: { bounds: Bounds }) => void레이아웃 변경 후 bounds에 부모 뷰 기준 프레임 { origin, size }가 전달됩니다.
ref.element_ref(&handle)ViewRef | (node: ViewRef) => void요소의 ref입니다(ref 메서드 참고). ViewRef 또는 이를 확장한 요소별 타입을 사용합니다. ref={el}은 변수에 할당하고 ref={fn}은 함수를 호출합니다.
nativeID.native_id(id)string다른 뷰에서 참조할 이름입니다. 예를 들어 입력란이 연결할 InputAccessoryView를 이 이름으로 지정합니다.
testID.test_id(id)stringUI 테스트에서 사용할 식별자입니다. Android에서는 뷰의 리소스 ID입니다.
key.key(key)string형제 요소 사이에서 해당 요소를 식별하는 키입니다.

#제스처로 애니메이션 값 제어

다음 속성은 제스처를 UI 스레드에서 움직이는 네이티브 애니메이션 값에 연결합니다. 앱 코드가 매 프레임 값을 갱신할 필요가 없습니다. 터치와 제스처를 참고하세요.

속성Rust타입설명
pan.pan_values(Some(&x), Some(&y)){ x?: AnimatedValue; y?: AnimatedValue }드래그가 포인트 단위의 값을 갱신합니다.
onPanStart.on_pan_start(..)(e: PanEvent) => void드래그가 시작되었습니다.
onPanEnd.on_pan_end(..)(e: PanEvent) => void드래그가 끝났습니다. 손을 뗄 때의 속도로 decay 등을 시작할 수 있습니다.
pinch.pinch_value(&scale){ scale?: AnimatedValue }두 손가락의 핀치 동작으로 scale을 조절합니다.
onPinchStart / onPinchEnd.on_pinch_start(..) / .on_pinch_end(..)(e: { scale: number; velocity: number }) => void핀치가 시작되거나 끝났습니다.
rotation.rotation_value(&angle){ angle?: AnimatedValue }두 손가락의 회전으로 angle을 도 단위로 조절합니다.
onRotationStart / onRotationEnd.on_rotation_start(..) / .on_rotation_end(..)(e: { rotation: number; velocity: number }) => void회전이 시작되거나 끝났습니다.

PanEvent는 { cancelled, translation, velocity, value? } 형태입니다. Rust에서는 cancelled, translation, velocity_x, velocity_y, value_x, value_y입니다. 시스템이 드래그를 취소했는지, 시작점 기준 변위와 초당 포인트 단위의 속도(Point), 그 순간 연결된 애니메이션 값(pan에 연결한 축별 { x?, y? })을 알려 줍니다. 애니메이션이 진행되는 중에도 드래그로 값을 이어받을 수 있습니다.

#터치와 리스폰더 시스템

위 속성으로 처리하기 어려운 제스처에는 모든 요소가 지원하는 터치·리스폰더 속성을 사용하세요. onTouchStart, onTouchMove, onTouchEnd, onTouchCancel 및 이들의 …Capture 형식, onStartShouldSetResponder, onMoveShouldSetResponder 및 …Capture 형식, onResponderGrant, onResponderReject, onResponderStart, onResponderMove, onResponderEnd, onResponderRelease, onResponderTerminate, onResponderTerminationRequest가 있습니다. 각 핸들러는 GestureResponderEvent({ identifier, locationX, locationY, pageX, pageY, target, timestamp, touches, changedTouches, nativeEvent, stopPropagation() })를 받으며 …ShouldSetResponder 핸들러에는 gestureState(dx, dy, vx, vy 등)도 전달됩니다. onResponderGrant에서 true를 반환하면 네이티브 스크롤 뷰가 터치를 가져가지 않습니다. panActivation({ activeOffsetX, activeOffsetY, failOffsetX, failOffsetY })을 설정하면 플랫폼이 팬 제스처를 활성화한 뒤에야 뷰가 리스폰더가 됩니다. 그래서 스크롤 뷰 안에서 드래그할 때 다른 축의 움직임은 스크롤 뷰에 맡길 수 있습니다(스크롤 뷰 안의 팬). PanResponder는 이 시스템을 기반으로 합니다. Rust에서는 .on_touch(..)가 모든 터치(TouchEvent)를 받고 .claim_touches(..)가 뷰가 터치를 맡을 시점을 결정합니다. 이후에는 .on_responder_grant(..), .on_responder_move(..), .on_responder_release(..) 등이 리스폰더의 상태를 따라 호출됩니다.

#접근성

속성Rust타입설명
accessible.accessible(..)boolean보조 기술에 이 뷰 전체를 하나의 요소로 노출합니다. 자식 요소는 따로 노출하지 않습니다.
accessibilityLabel.label(..)string화면 읽기 프로그램이 읽을 이름입니다. 레이블을 생략하면 뷰의 텍스트를 읽습니다.
accessibilityHint.hint(..)string요소를 활성화했을 때 어떤 동작이 일어나는지 설명합니다.
role.role(AccessibilityRole::Button)AccessibilityRole요소의 역할을 지정합니다("button", "header", "link", "switch" 등).
accessibilityState.accessibility_state(..){ disabled?, selected?, checked?, busy?, expanded?, pressed?, current? }보조 기술이 알려 주는 상태입니다. checked와 pressed는 "mixed"일 수 있고, current는 true 또는 "page", "step", "location", "date", "time"입니다.
accessibilityLevel.accessibility_level(..)number제목 수준 또는 트리 항목의 깊이입니다. 최상위 값은 1입니다.
accessibilityValue.accessibility_value(..){ min?, max?, now?, text? }범위 또는 텍스트 요소의 값입니다.
accessibilityElementsHiddenboolean보조 기술에서 뷰와 그 하위 트리를 숨깁니다.
importantForAccessibility"auto" | "yes" | "no" | "no-hide-descendants"뷰를 보조 기술에 노출할지 지정합니다.
accessibilityActions{ name: string; label?: string }[]보조 기술에 제공할 사용자 정의 동작입니다.
onAccessibilityAction.on_accessibility_action(..)(e: { actionName: string }) => void사용자가 accessibilityActions 중 하나를 실행했습니다.

접근성을 참고하세요.

#플랫폼 전용 속성

TSX에서는 한 플랫폼에만 있는 속성을 해당 플랫폼 이름의 객체에 넣습니다. 공통 속성은 모든 플랫폼에서 동일하고 현재 플랫폼의 객체만 전달됩니다. Rust 빌더에서는 각 플랫폼에서만 적용되는 메서드를 사용합니다.

속성설명
ios={{…}}iOS 및 Mac Catalyst에 적용됩니다. <Input>, Dialog, WebView는 각각 전용 키도 제공합니다.
android={{…}}Android에 적용됩니다. 모든 요소가 아래의 ripple, elevation, accessibilityLiveRegion을 지원합니다.
desktop={{…}}데스크톱 플랫폼과 데스크톱 브라우저에 적용되며 모바일에서는 무시합니다. 모든 요소가 아래의 windowDrag를 지원합니다.
web={{…}}브라우저에서만 적용됩니다.
android 키Rust타입설명
ripple.ripple(Ripple { .. }){ color?, borderless?, radius?, foreground? }눌렀을 때 나타나는 머티리얼 리플 효과입니다.
elevation.elevation(px(2.0))number뷰의 모양을 따르는 머티리얼 그림자의 높이(포인트)입니다.
accessibilityLiveRegion"none" | "polite" | "assertive"뷰의 변경 사항을 TalkBack이 어떻게 안내할지 정합니다.
desktop 키Rust타입설명
windowDrag.window_drag(..)boolean제목 표시줄처럼 뷰를 눌러 창을 끌 수 있습니다. 드래그가 가능한 뷰의 일부를 클릭 가능한 상태로 두려면 false로 설정하세요. 사용자 정의 제목 표시줄을 참고하세요.
use nsh_core::Ripple;

view()
    .rounded_lg()
    .bg(white())
    .p_4()
    .elevation(px(2.0))
    .ripple(Ripple { color: Some(rgba(0x00000022).into()), ..Ripple::default() })
    .on_press(save)
    .child(text("Save"))

플랫폼별 코드를 참고하세요.

#ref 메서드

TSX에서 ref는 해당 요소 고유 타입의 참조를 받습니다. View, Text, Image, Canvas, Overlay, Shader는 focus, blur, measure를 갖는 ViewRef를 사용합니다. 그 밖의 요소는 ViewRef를 확장한 타입으로 전용 메서드를 제공합니다. Input은 InputRef, Scroll은 ScrollRef, List는 ListRef, WebView는 WebViewRef, Video는 VideoRef, 네이티브 컴포넌트는 NativeComponentRef입니다. Rust의 element_ref()는 .element_ref(&handle)로 연결하는 ElementRef 핸들을 만듭니다. ref 메서드로 보낸 명령은 같은 실행 차례에서 앞서 발생한 변경에 이어 순서대로 네이티브 뷰에 전달됩니다. 각 요소의 문서에서 지원 메서드를 확인하세요. ref 변수에는 요소 고유의 타입을 지정해야 합니다(<Input>이라면 let field!: InputRef). 고유 메서드가 있는 요소의 ref를 ViewRef 타입 변수에 연결할 수 없습니다.

메서드Rustref 타입설명
focus() / blur()focus() / blur()ViewRef<Input>에 포커스를 주어 키보드를 표시하거나, 누를 수 있는 뷰에 키보드 포커스를 주거나 해제합니다.
measure()measure()ViewRef뷰의 위치를 측정합니다.
setSelection(start, end?)set_selection(start, end)InputRefUTF-16 단위로 start..end 구간을 선택합니다. end를 생략하면 커서만 놓습니다.
scrollTo({ x?, y?, animated? })scroll_to(point, animated)ScrollRef, ListRef지정한 오프셋으로 스크롤합니다.
zoomTo({ scale, x?, y?, animated? })zoom_to(scale, center, animated)ScrollRef(확대·축소 가능한 <Scroll>)콘텐츠의 한 점을 중심으로 배율을 scale로 조정합니다.
scrollToOffset({ offset, animated? })scroll_to_offset(offset, animated)ListRef목록의 스크롤 축을 따라 지정한 오프셋으로 이동합니다.
scrollToIndex({ index, animated?, viewPosition?, viewOffset? })scroll_to_index(index, position, offset, animated)ListRef항목이 보이도록 스크롤합니다.
scrollToEnd({ animated? })scroll_to_end(animated)ScrollRef, ListRef콘텐츠 끝으로 스크롤합니다.
reload(), goBack(), goForward(), stopLoading(), injectJavaScript(script), postMessage(data)reload(), go_back(), go_forward(), stop_loading(), inject_java_script(..), post_message(..)WebViewRef페이지를 제어합니다.
seek(seconds)seek(seconds)VideoRef지정한 재생 시점으로 이동합니다.
command(name, args?)command(name, args)NativeComponentRef등록한 네이티브 컴포넌트의 명령을 호출합니다(nativeComponent).
let field = element_ref();

view()
    .on_press(move || field.focus())
    .child(input().element_ref(&field).placeholder("Tap the row to type"))

#핸들러가 UI를 차단하지 않는 이유

메인 스레드는 앱 코드의 실행이 끝나기를 기다리지 않습니다. 시스템이 즉시 답을 요구하면(텍스트를 바꿀 수 있는지, 뒤로 가기 버튼으로 화면을 닫을 수 있는지, 시트를 닫아도 되는지 등) NSH가 미리 설정한 maxLength, popOnBack, closeOnSwipe 등의 속성을 기준으로 네이티브에서 판단하고 이후에 이벤트를 앱에 전달합니다. 이런 동작을 핸들러의 반환값 대신 속성으로 지정하는 이유입니다.