레이아웃과 CSS
이미 쓰고 있는 CSS 그대로
레이아웃은 blitz-dom 이 합니다. Stylo 가 CSS 를 풀고 Taffy 가 상자를 놓으며, Dioxus 자체의 네이티브 렌더러가 쓰는 엔진과 같습니다. 그래서 지원 범위가 넓고, 이 페이지는 속성 목록 대신 예제로 그 범위를 보여 줍니다.
스타일시트
스타일시트는 HtmlConfig::stylesheets 에 넣습니다. 각각이
<head> 안의 <style> 인 것처럼 사용자 에이전트 스타일시트
다음에 순서대로 적용됩니다. style 속성도 동작합니다. 사용자 에이전트 스타일시트는
브라우저와 비슷한 blitz-dom 의 것이고(body { margin: 8px }), 빈 상태에서 시작하고
싶으면 HtmlConfig::user_agent_stylesheet 로 바꿉니다.
let dom = HtmlDom::with_config(app, HtmlConfig {
stylesheets: vec![STYLE.to_string()],
..HtmlConfig::default()
});
블록과 인라인 흐름
블록은 위에서 아래로 쌓이고 세로 여백은 겹칩니다. samples/hello 에서 제목의 아래
여백 16px 와 문단의 위 여백 12px 가 16px 로 겹치므로, 문단은 y = 108 이 아니라 y = 96 에
놓입니다. 보통 글자, strong, em 이 섞인 문단은 스타일마다 조각 하나씩인
인라인 서식 문맥 하나이고, 모두 한 줄에 놓입니다.
p { id: "intro",
"This page is "
strong { id: "bold", "HTML" }
" and "
em { id: "italic", "CSS" }
", laid out by the Host."
}
Flexbox
samples/dashboard 는 처음부터 끝까지 flexbox 입니다. 페이지는 세로, 상단 바는 가로,
사이드바는 세로, 카드는 줄바꿈되는 가로입니다. 1000px 뷰포트에서는 200px 카드 세 개가 한 줄에
들어가고, 700px 에서는 두 개만 들어가 나머지가 다음 줄로 갑니다. 테스트가 두 경우를 모두
확인합니다.
.app { display: flex; flex-direction: column; height: 100vh; }
.topbar {
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
height: 56px;
}
.body { display: flex; flex: 1; align-items: stretch; min-height: 0; }
.cards {
display: flex;
flex-wrap: wrap;
gap: 16px;
justify-content: flex-start;
align-items: flex-start;
}
min-width: fit-content 도 동작합니다. samples/workbench 의 탭은 이름이
더 필요하지 않으면 120px 이므로, layout_engine.rs 탭은 185px 가 되고
main.rs 탭은 120px 그대로입니다.
Grid
samples/gallery 는 repeat(auto-fill, minmax(160px, 1fr)) 를 씁니다.
800px 뷰포트에서는 183px 열 네 개가 되고, grid-column: span 2 인 타일은 그중 두 개를
차지합니다(378px).
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
grid-auto-rows: 140px;
gap: 12px;
padding: 16px;
}
.tile.wide { grid-column: span 2; }
표
table, thead, tbody, tr, th,
td 는 표로 배치됩니다. samples/table 에서 열 폭은 머리글 칸에서 오고,
border-collapse: collapse 가 테두리를 나눠 쓰며, text-align: right 가
금액을 오른쪽 끝에 붙입니다. 표의 최소 폭이 640px 이므로 480px 페이지에서는 감싼 상자 안에서
옆으로 스크롤됩니다(스크롤과 오버레이 참고).
위치 지정과 쌓임
position: relative, absolute, fixed 가 동작하고,
z-index 가 그리는 순서와 클릭이 닿는 상자를 함께 정합니다.
samples/overlay 에서 메뉴는 버튼을 감싼 상자의 top: 100% 에 놓여 그
아래 내용 위에 그려지고, 대화 상자는 고정된 배경 위에서 뷰포트 가운데에 고정됩니다.
.menu-anchor { position: relative; display: flex; }
.menu { position: absolute; top: 100%; left: 0; z-index: 10; width: 180px; }
.dialog {
position: fixed;
top: 50%;
left: 50%;
z-index: 101;
width: 320px;
transform: translate(-50%, -50%);
}
대화 상자의 translate(-50%, -50%) 는 이동을 상자 위치에 더하므로 동작합니다. 다른
변환은 아직 그리지 않습니다. 아래를 보십시오.
배경, 테두리, 효과
모든 상자는 계산된 장식을 그리기 목록에 담습니다. 배경색과 배경 이미지
(이미지와 배경), 변마다 폭과 색이 따로인 테두리, CSS 가 겹치는
반지름을 줄이는 방식대로 줄인 모서리 반지름, box-shadow, 조상까지 곱한
opacity 입니다. visibility: hidden 은 자리를 지키되 아무것도 그리지
않고, display: none 은 상자를 없앱니다.
.card {
width: 200px;
height: 120px;
padding: 16px;
background: #ffffff;
border: 1px solid #d0d7de;
border-radius: 12px;
box-shadow: 0 1px 3px rgba(31, 35, 40, 0.2);
}
.card.alert { border-left: 4px solid #cf222e; }
.card.stale { opacity: 0.6; }
렌더러가 받게 될 것
렌더러는 CSS 를 받지 않습니다. plan_from 이 그리기 목록을 그리기 원소로 바꾸고,
그 과정의 선택들이 트리를 작게 유지합니다.
- 네 변이 같은 테두리는 테두리 하나, 다르면 변마다 하나가 됩니다. 반지름이 모두 같으면 모양 하나, 다르면 모서리마다 반지름 하나가 됩니다.
- 절대 위치는 부모 상자 안의 오프셋이 됩니다. 그래서 상자 하나가 움직이면 자손 전부가 아니라 오프셋 하나만 바뀝니다.
overflow: auto나scroll은 내용을 담은 스크롤 컨테이너가 됩니다.opacity는 그룹 전체의 알파 하나가 되고,z-index는 자식 순서가 됩니다.- 갱신 뒤에
diff는 바뀐 것만 알려 줍니다. 상자 하나의 배경을 바꾸면 계획에서 바뀌는 것은 하나이고, 테스트가 바로 그것을 확인합니다.
아직 없는 것
| CSS | 지금 | 상태 |
|---|---|---|
transform: rotate, scale, skew, matrix(), transform-origin | Stylo 가 계산하지만 상자에는 이동만 반영됩니다. 다른 변환이 있는 상자는 레이아웃 자리에 그대로 있습니다. | 계획 #46 |
transition, animation, @keyframes | 애니메이션 값이 모두 첫 값에 멈춰 있습니다. | 계획 #46(불투명도, 변환, 색), #47(폭, 높이, 여백) |
| VS Code 같은 페이지 확대(Cmd/Ctrl + +) | 페이지는 layout 에 넘긴 배율로 배치됩니다. | 계획 |
애니메이션은 무엇을 움직이느냐로 나뉠 예정입니다. 레이아웃을 바꾸지 않는 속성(불투명도, 변환, 색)은 렌더러가 Compose 의 애니메이션 시계로 재생하므로 Host 는 프레임마다 할 일이 없습니다. 레이아웃을 바꾸는 속성은 Host 가 프레임마다 다시 계산합니다. 다른 것이 모두 어디로 움직이는지는 레이아웃 엔진만 알기 때문입니다.