pub fn join_vertical(align: Position, surfaces: &[Surface]) -> SurfaceExpand description
Join surfaces stacked vertically, aligning them horizontally.
All input surfaces are placed in a single column, aligned according to align.
§Example
use bobatea::surface::{Surface, join_vertical, Position};
let top = Surface::from_text("ABC", Default::default());
let bottom = Surface::from_text("X", Default::default());
let combined = join_vertical(Position::Center, &[top, bottom]);
assert_eq!(combined.height(), 2);Examples found in repository?
examples/hello_layout.rs (line 68)
37 fn render(&self, ctx: &mut Frame<'_>, _theme: &Theme) {
38 let area = ctx.area();
39 let buf = ctx.buffer_mut();
40
41 // Clear background
42 for y in area.top()..area.bottom() {
43 for x in area.left()..area.right() {
44 buf[(x, y)].reset();
45 buf[(x, y)].set_bg(Color::Black);
46 }
47 }
48
49 // Build some styled text surfaces
50 let hello = BobaStyle::new().fg(hex_color("#FF5F87")).bg(Color::Black).bold().render("Hello");
51
52 let world = BobaStyle::new().fg(hex_color("#A550DF")).bg(Color::Black).italic().render("World");
53
54 // Join them horizontally with a divider
55 let divider = BobaStyle::new().fg(Color::DarkGray).render("│");
56 let joined = join_horizontal(Position::Center, &[hello, divider, world]);
57
58 // Put a rounded border around it
59 let boxed = BobaStyle::new()
60 .border(Border::rounded())
61 .border_fg(hex_color("#874BFD"))
62 .padding(1, 2, 1, 2)
63 .render_surface(&joined);
64
65 // Add a small label below
66 let label = BobaStyle::new().fg(Color::DarkGray).margin_top(1).render("press 'q' to quit");
67
68 let doc = join_vertical(Position::Center, &[boxed, label]);
69
70 // Center the whole thing on screen
71 let centered = place(
72 area.width as usize,
73 area.height as usize,
74 Position::Center,
75 Position::Center,
76 &doc,
77 &Cell::blank(Style::default().bg(Color::Black)),
78 );
79
80 // Render into the frame buffer
81 centered.render_to_area(area, buf);
82 }More examples
examples/layout.rs (line 178)
145 fn build_document(&self) -> Surface {
146 let has_dark_bg = has_dark_background();
147 let subtle = light_dark(has_dark_bg, hex_color("#D9DCCF"), hex_color("#383838"));
148 let highlight = light_dark(has_dark_bg, hex_color("#874BFD"), hex_color("#7D56F4"));
149 let special = light_dark(has_dark_bg, hex_color("#43BF6D"), hex_color("#73F59F"));
150
151 // Tabs
152 let tabs_row = self.tabs.render_to_surface();
153
154 // Title
155 let title_style = BobaStyle::new()
156 .margin_left(1)
157 .margin_right(5)
158 .padding(0, 1, 0, 1)
159 .italic()
160 .fg(hex_color("#FFF7DB"));
161 let title_colors = blend_2d(
162 1,
163 5,
164 hex_color("#F25D94"),
165 hex_color("#643AFF"),
166 hex_color("#EDFF82"),
167 hex_color("#14F9D5"),
168 );
169 let mut title_surfaces = Vec::new();
170 for (i, row) in title_colors.iter().enumerate() {
171 let color = row[0];
172 let s = title_style
173 .margin_left((i * 2) as u16)
174 .bg(color)
175 .render("Boba Tea");
176 title_surfaces.push(s);
177 }
178 let title = join_vertical(Position::Left, &title_surfaces);
179
180 let desc_style = BobaStyle::new().margin_top(1);
181 let info_style = BobaStyle::new()
182 .border(Border::normal().no_left().no_right().no_bottom())
183 .border_fg(subtle);
184 let divider = BobaStyle::new().padding(0, 1, 0, 1).fg(subtle).render("•");
185 let url = BobaStyle::new()
186 .fg(special)
187 .render("https://github.com/tascord/boba");
188 let info_content = join_horizontal(
189 Position::Top,
190 &[
191 BobaStyle::new().render("by flora (based on Charm)"),
192 divider,
193 url,
194 ],
195 );
196 let info = info_style.render_surface(&info_content);
197 let desc = join_vertical(
198 Position::Left,
199 &[
200 desc_style.render("Terminal layout + styling you won't hate."),
201 info,
202 ],
203 );
204 let title_row = join_horizontal(Position::Top, &[title, desc]);
205
206 // Dialog
207 let dialog_box = BobaStyle::new()
208 .border(Border::rounded())
209 .border_fg(hex_color("#874BFD"))
210 .padding(1, 4, 1, 4)
211 .align(Alignment::Center, Position::Center);
212
213 let question = BobaStyle::new().render_surface(&apply_gradient(
214 "Are you sure you want to eat marmalade?",
215 hex_color("#EDFF82"),
216 hex_color("#F25D94"),
217 ));
218 let ok_surf = self.ok_btn.render_to_surface(&Theme::default());
219 let cancel_surf = self.cancel_btn.render_to_surface(&Theme::default());
220 let buttons = join_horizontal(Position::Top, &[ok_surf, cancel_surf]);
221 let dialog_content = join_vertical(Position::Center, &[question, buttons]);
222 let dialog_inner = dialog_box.render_surface(&dialog_content);
223 let dialog = place_filled(
224 WIDTH,
225 DIALOG_HEIGHT,
226 Position::Center,
227 Position::Center,
228 &dialog_inner,
229 Style::default().fg(subtle),
230 "l o r e m ",
231 );
232 let mut dialog = dialog;
233 fit_width(
234 &mut dialog,
235 WIDTH,
236 &Cell::blank(Style::default().fg(subtle)),
237 );
238
239 // Color grid
240 let colors = color_grid_surf(14, 8);
241
242 // Lists
243 let list_header_style = BobaStyle::new()
244 .border(Border::normal().no_top().no_left().no_right())
245 .border_fg(subtle)
246 .margin_right(2);
247 let list_style = BobaStyle::new()
248 .border(Border::normal().no_top().no_bottom().no_right())
249 .border_fg(subtle)
250 .margin_right(1)
251 .height(8)
252 .width((WIDTH / 3) as u16);
253
254 let check = BobaStyle::new().fg(special).padding_right(1).render("✓");
255 let gray_done = light_dark(has_dark_bg, hex_color("#969B86"), hex_color("#696969"));
256 let list_done = |text: &str| -> Surface {
257 let body = BobaStyle::new().crossed_out().fg(gray_done).render(text);
258 join_horizontal(Position::Top, &[check.clone(), body])
259 };
260 let list_item = |text: &str| -> Surface { BobaStyle::new().padding_left(2).render(text) };
261
262 let list1 = join_vertical(
263 Position::Left,
264 &[
265 list_header_style.render("Citrus Fruits to Try"),
266 list_done("Grapefruit"),
267 list_done("Yuzu"),
268 list_item("Citron"),
269 list_item("Kumquat"),
270 list_item("Pomelo"),
271 ],
272 );
273 let list1 = list_style.render_surface(&list1);
274
275 let list2 = join_vertical(
276 Position::Left,
277 &[
278 list_header_style.render("Actual Boba Vendors"),
279 list_item("Chatime"),
280 list_item("Gong Cha"),
281 list_done("Teaser"),
282 ],
283 );
284 let list2 = list_style.render_surface(&list2);
285
286 let lists = join_horizontal(Position::Top, &[list1, list2, colors]);
287
288 // History
289 let history_style = BobaStyle::new()
290 .align(Alignment::Left, Position::Top)
291 .fg(hex_color("#FAFAFA"))
292 .bg(highlight)
293 .margin(1, HISTORY_GAP as u16, 0, 0)
294 .padding(1, 2, 1, 2)
295 .height(19)
296 .width(HISTORY_CONTENT_WIDTH as u16);
297
298 let history_a = history_style
299 .clone()
300 .align(Alignment::Right, Position::Top)
301 .render(LOREM);
302 let history_b = history_style
303 .clone()
304 .align(Alignment::Center, Position::Top)
305 .render(LOREM);
306 let history_c = history_style
307 .align(Alignment::Left, Position::Top)
308 .margin_right(0)
309 .render(LOREM);
310 let history = join_horizontal(Position::Top, &[history_a, history_b, history_c]);
311
312 // Status bar
313 let light_dark_state = if has_dark_bg { "Dark" } else { "Light" };
314 let bar_style = BobaStyle::new()
315 .fg(light_dark(
316 has_dark_bg,
317 hex_color("#343433"),
318 hex_color("#C1C6B2"),
319 ))
320 .bg(light_dark(
321 has_dark_bg,
322 hex_color("#D9DCCF"),
323 hex_color("#353533"),
324 ));
325
326 let status_key = status_seg(hex_color("#FF5F87")).render("STATUS");
327 let encoding = status_seg(hex_color("#A550DF"))
328 .align(Alignment::Right, Position::Center)
329 .render("UTF-8");
330 let fish = status_seg(hex_color("#6124DF")).render("Fish Cake");
331
332 // Segment widths already include their horizontal padding.
333 let used = status_key.width() + encoding.width() + fish.width();
334 let remaining = WIDTH.saturating_sub(used);
335 let status_text_width = remaining.saturating_sub(2);
336
337 let mut status_val = BobaStyle::new()
338 .inherit(bar_style)
339 .width(status_text_width as u16)
340 .padding_x(1)
341 .render(&format!("Ravishingly {}!", light_dark_state));
342 clip(&mut status_val, remaining);
343 fit_width(&mut status_val, remaining, &Cell::blank(*bar_style));
344
345 let bar = join_horizontal(Position::Top, &[status_key, status_val, encoding, fish]);
346 let mut status_bar = bar;
347 let fill_cell = Cell::blank(
348 Style::new()
349 .fg(light_dark(
350 has_dark_bg,
351 hex_color("#343433"),
352 hex_color("#C1C6B2"),
353 ))
354 .bg(light_dark(
355 has_dark_bg,
356 hex_color("#D9DCCF"),
357 hex_color("#353533"),
358 )),
359 );
360 fit_width(&mut status_bar, WIDTH, &fill_cell);
361
362 // Assemble document
363 join_vertical(
364 Position::Left,
365 &[tabs_row, title_row, dialog, lists, history, status_bar],
366 )
367 }