스크롤과 오버레이
스크롤 컨테이너, 클립, 층
무엇이 스크롤되고 무엇이 잘리는지는 overflow 가, 무엇이 페이지 위에 놓이는지는 위치
지정이 정합니다. Host 는 스크롤 컨테이너의 내용 전체를 한 번 배치하고, 스크롤은 렌더러가 합니다.
Rust 가 스크롤 위치를 가지면 늘 한 프레임 늦기 때문입니다.
스크롤 컨테이너
어떤 축에 overflow: auto 나 scroll 이 있는 상자는 그 축으로 스크롤됩니다.
그리기 목록의 항목에는 스크롤 컨테이너가 붙습니다. 들여다보는 뷰포트, 안에 든 것 전체의 크기,
스크롤되는 축입니다. 다른 축의 overflow: hidden 은 스크롤 없이 자르기만 합니다. 안에
있는 상자는 모두 가장 가까운 스크롤 조상을 기록하므로, 렌더러는 스크롤 위치마다 무엇이 같이
움직이는지 압니다.
.chat { display: flex; flex-direction: column; height: 100%; }
.messages {
flex: 1;
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px;
}
.composer { display: flex; gap: 8px; padding: 12px; border-top: 1px solid #e2e8f0; }
samples/chat 에서 메시지 목록은 입력 줄 위의 공간을 차지하고 세로로만 스크롤됩니다.
브라우저에서처럼 min-height: 0 이 중요합니다. 없으면 flex 항목이 내용보다 작아지지
않아서 목록이 입력 줄을 아래로 밀어냅니다.
samples/table 은 반대 방향으로 스크롤됩니다. 표의 최소 폭이 640px 이므로, 480px
페이지에서는 432px 폭의 감싼 상자가 640px 전체를 옆으로 스크롤합니다.
.table-wrap {
overflow-x: auto;
overflow-y: hidden;
border-radius: 8px;
}
.invoices { width: 100%; min-width: 640px; border-collapse: collapse; }
길어지는 대화
어시스턴트의 답은 조각조각 도착합니다. samples/chat 은 대화를 컨텍스트로 제공한
시그널에 두고, Chat::receive 가 조각마다 마지막 메시지에 덧붙입니다. 바뀌는 것은 그
메시지의 항목 하나, 그 글자와 크기뿐입니다. 목록의 다른 것은 다시 보내지 않습니다.
pub fn receive(mut self, chunk: &str) {
let mut messages = self.messages.write();
if let Some(last) = messages
.last_mut()
.filter(|last| last.from == Sender::Assistant)
{
last.text.push_str(chunk);
return;
}
// A piece that arrives after the user has spoken starts a new reply.
}
테스트는 "Sure" 에 ", where to?" 를 덧붙이고, 레이아웃 diff 에 바뀐 항목이 답 하나뿐이고 삭제도 순서 변경도 없음을 확인합니다.
클리핑
overflow 가 visible 이 아닌 상자는 자손을 자기 패딩 상자와 둥근 모서리로
자릅니다. 자손의 항목에는 조상이 거는 클립이 기록되고, 그 클립 밖을 누르면 자손에 닿지 않습니다.
samples/overlay 에서 240 x 80 미리보기 안의 400 x 160 배너는 미리보기 크기로 잘리고,
미리보기 밖에서 배너가 있었을 자리를 눌러도 배너에 닿지 않습니다.
.preview {
width: 240px;
height: 80px;
overflow: hidden;
border-radius: 8px;
}
.preview-banner { width: 400px; height: 160px; background-color: #6366f1; }
메뉴와 대화 상자
오버레이는 보통의 HTML 입니다. if 로 보이고, 위치 지정으로 놓고,
z-index 로 쌓습니다. 그리는 순서와 적중 판정이 모두 쌓임을 따르므로, 배경을 누르면
그 아래 버튼이 아니라 배경에 닿습니다.
if show_dialog {
div { id: "backdrop", class: "backdrop", onclick: move |_| dialog_open.set(false) }
div { id: "dialog", class: "dialog", role: "dialog",
h2 { id: "dialog-title", "Delete project?" }
p { "This cannot be undone." }
div { class: "actions",
button { id: "cancel", onclick: move |_| dialog_open.set(false), "Cancel" }
button {
id: "confirm",
class: "danger",
onclick: move |_| {
deleted.set(true);
dialog_open.set(false);
},
"Delete"
}
}
}
}
.backdrop {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 100;
background-color: #0f172a;
opacity: 0.5;
}
position: fixed 상자는 문서 어디에 있든 뷰포트를 기준으로 놓입니다. 그래서 배경은 창
전체를 덮고, 대화 상자는 그 가운데에 놓입니다.