Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_text/tests/annealer_corpus/ratatui_widgets.rs

89.5 KiB, 1 run

created by r1870400018:11780, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! Elements related to the `Block` base widget.
2//!
3//! This holds everything needed to display and configure a [`Block`].
4//!
5//! In its simplest form, a `Block` is a [border](Borders) around another widget. It can have a
6//! [title](Block::title) and [padding](Block::padding).
7
8use alloc::vec::Vec;
9
10use itertools::Itertools;
11use ratatui_core::buffer::Buffer;
12use ratatui_core::layout::{Alignment, Rect};
13use ratatui_core::style::{Style, Styled};
14use ratatui_core::symbols::border;
15use ratatui_core::symbols::merge::MergeStrategy;
16use ratatui_core::text::Line;
17use ratatui_core::widgets::Widget;
18use strum::{Display, EnumString};
19
20pub use self::padding::Padding;
21pub use self::shadow::{CellEffect, Dimmed, Shadow, dimmed};
22use crate::borders::{BorderType, Borders};
23
24mod padding;
25mod shadow;
26
27/// A widget that renders borders, titles, and padding around other widgets.
28///
29/// A `Block` is a foundational widget that creates visual containers by drawing borders around an
30/// area. It serves as a wrapper or frame for other widgets, providing structure and visual
31/// separation in terminal UIs. Most built-in widgets in Ratatui use a pattern where they accept an
32/// optional `Block` parameter that wraps the widget's content.
33///
34/// When a widget renders with a block, the widget's style is applied first, then the block's style,
35/// and finally the widget's content is rendered within the inner area calculated by the block. This
36/// layered approach allows for flexible styling where the block can provide background colors,
37/// borders, and padding while the inner widget handles its own content styling.
38///
39/// Multiple blocks can be nested within each other. The [`Block::inner`] method calculates the area
40/// available for content after accounting for borders, titles, and padding, making it easy to nest
41/// blocks or position widgets within a block's boundaries.
42///
43/// # Constructor Methods
44///
45/// - [`Block::new`] - Creates a block with no borders or padding
46/// - [`Block::bordered`] - Creates a block with all borders enabled
47///
48/// # Border Configuration
49///
50/// - [`Block::borders`] - Specifies which borders to display
51/// - [`Block::border_style`] - Sets the style of the borders
52/// - [`Block::border_type`] - Sets border symbols (single, double, thick, rounded, etc.)
53/// - [`Block::border_set`] - Sets custom border symbols as a [`border::Set`]
54/// - [`Block::merge_borders`] - Controls how borders merge with adjacent blocks
55///
56/// # Title Configuration
57///
58/// - [`Block::title`] - Adds a title to the block
59/// - [`Block::title_top`] - Adds a title to the top of the block
60/// - [`Block::title_bottom`] - Adds a title to the bottom of the block
61/// - [`Block::title_alignment`] - Sets default alignment for all titles
62/// - [`Block::title_style`] - Sets the style for all titles
63/// - [`Block::title_position`] - Sets default position for titles
64///
65/// # Styling and Layout
66///
67/// - [`Block::style`] - Sets the base style of the block
68/// - [`Block::shadow`] - Adds a shadow rendered behind the block
69/// - [`Block::padding`] - Adds internal padding within the borders
70/// - [`Block::inner`] - Calculates the inner area available for content
71///
72/// # Title Behavior
73///
74/// You can add multiple titles to a block, and they will be rendered with spaces separating titles
75/// that share the same position or alignment. When both centered and non-centered titles exist, the
76/// centered space is calculated based on the full width of the block.
77///
78/// Titles are set using the `.title`, `.title_top`, and `.title_bottom` methods. These methods
79/// accept a string or any type that can be converted into a [`Line`], such as a string slice,
80/// `String`, or a vector of [`Span`]s. To control the alignment of a title (left, center, right),
81/// pass a `Line` with the desired alignment, e.g. `Line::from("Title").centered()`.
82///
83/// By default, `.title` places the title at the top of the block, but you can use `.title_top` or
84/// `.title_bottom` to explicitly set the position. The default alignment for all titles can be set
85/// with [`Block::title_alignment`], and the default position for all titles can be set with
86/// [`Block::title_position`].
87///
88/// Note that prior to `v0.30.0`, the `block::Title` struct was used to create titles. This struct
89/// has been removed. The new recommended approach is to use [`Line`] with a specific alignment for
90/// the title's content and the [`Block::title_top`] and [`Block::title_bottom`] methods for
91/// positioning.
92///
93/// Titles avoid being rendered in corners when borders are present, but will align to edges when no
94/// border exists on that side:
95///
96/// ```plain
97/// ┌With at least a left border───
98///
99/// Without left border───
100/// ```
101///
102/// # Nesting Widgets with `inner`
103///
104/// The [`Block::inner`] method computes the area inside the block after accounting for borders,
105/// titles, and padding. This allows you to nest widgets inside a block by rendering the block
106/// first, then rendering other widgets in the returned inner area.
107///
108/// For example, you can nest a block inside another block:
109///
110/// ```
111/// use ratatui::Frame;
112/// use ratatui::widgets::Block;
113///
114/// # fn render_nested_block(frame: &mut Frame) {
115/// let outer_block = Block::bordered().title("Outer");
116/// let inner_block = Block::bordered().title("Inner");
117///
118/// let outer_area = frame.area();
119/// let inner_area = outer_block.inner(outer_area);
120///
121/// frame.render_widget(outer_block, outer_area);
122/// frame.render_widget(inner_block, inner_area);
123/// # }
124/// ```
125///
126/// You can also use the standard [`Layout`] functionality to further subdivide the inner area and
127/// lay out multiple widgets inside a block.
128///
129/// # Integration with Other Widgets
130///
131/// Most widgets in Ratatui accept a block parameter. For example, [`Paragraph`], [`List`],
132/// [`Table`], and other widgets can be wrapped with a block:
133///
134/// ```
135/// use ratatui::widgets::{Block, Paragraph};
136///
137/// let paragraph = Paragraph::new("Hello, world!").block(Block::bordered().title("My Paragraph"));
138/// ```
139///
140/// This pattern allows widgets to focus on their content while blocks handle the visual framing.
141///
142/// # Styling
143///
144/// Styles are applied in a specific order: first the block's base style, then border styles, then
145/// title styles, and finally any content widget styles. This layered approach allows for flexible
146/// styling where outer styles provide defaults that inner styles can override.
147///
148/// `Block` implements [`Stylize`](ratatui_core::style::Stylize), allowing you to use style
149/// shorthand methods:
150///
151/// ```
152/// use ratatui::style::Stylize;
153/// use ratatui::widgets::Block;
154///
155/// let block = Block::bordered().red().on_white().bold();
156/// ```
157///
158/// # Examples
159///
160/// Create a simple bordered block:
161///
162/// ```
163/// use ratatui::widgets::Block;
164///
165/// let block = Block::bordered().title("My Block");
166/// ```
167///
168/// Create a block with custom border styling:
169///
170/// ```
171/// use ratatui::style::{Color, Style, Stylize};
172/// use ratatui::widgets::{Block, BorderType};
173///
174/// let block = Block::bordered()
175/// .title("Styled Block")
176/// .border_type(BorderType::Rounded)
177/// .border_style(Style::new().cyan())
178/// .style(Style::new().on_black());
179/// ```
180///
181/// Use a block to wrap another widget:
182///
183/// ```
184/// use ratatui::widgets::{Block, Paragraph};
185///
186/// let paragraph = Paragraph::new("Hello, world!").block(Block::bordered().title("Greeting"));
187/// ```
188///
189/// Add multiple titles with different alignments:
190///
191/// ```
192/// use ratatui::text::Line;
193/// use ratatui::widgets::Block;
194///
195/// let block = Block::bordered()
196/// .title_top(Line::from("Left").left_aligned())
197/// .title_top(Line::from("Center").centered())
198/// .title_top(Line::from("Right").right_aligned())
199/// .title_bottom("Status: OK");
200/// ```
201///
202/// # See Also
203///
204/// - [Block recipe] - Visual examples and common patterns (on the ratatui website)
205/// - [Collapse borders recipe] - Techniques for creating seamless layouts (on the ratatui website)
206/// - [`MergeStrategy`] - Controls how borders merge with adjacent elements
207///
208/// [Block recipe]: https://ratatui.rs/recipes/widgets/block/
209/// [Collapse borders recipe]: https://ratatui.rs/recipes/layout/collapse-borders/
210/// [`Paragraph`]: crate::paragraph::Paragraph
211/// [`Span`]: ratatui_core::text::Span
212/// [`Table`]: crate::table::Table
213/// [`Stylize`]: ratatui_core::style::Stylize
214/// [`List`]: crate::list::List
215/// [`Layout`]: ratatui_core::layout::Layout
216#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
217pub struct Block<'a> {
218 /// List of titles
219 titles: Vec<(Option<TitlePosition>, Line<'a>)>,
220 /// The style to be patched to all titles of the block
221 titles_style: Style,
222 /// The default alignment of the titles that don't have one
223 titles_alignment: Alignment,
224 /// The default position of the titles that don't have one
225 titles_position: TitlePosition,
226 /// Visible borders
227 borders: Borders,
228 /// Border style
229 border_style: Style,
230 /// The symbols used to render the border. The default is plain lines but one can choose to
231 /// have rounded or doubled lines instead or a custom set of symbols
232 border_set: border::Set<'a>,
233 /// Widget style
234 style: Style,
235 /// Block padding
236 padding: Padding,
237 /// Border merging strategy
238 merge_borders: MergeStrategy,
239 /// Block shadow
240 shadow: Option<Shadow>,
241}
242
243/// Defines the position of the title.
244///
245/// The title can be positioned on top or at the bottom of the block.
246///
247/// # Example
248///
249/// ```
250/// use ratatui::widgets::{Block, TitlePosition};
251///
252/// Block::bordered()
253/// .title_position(TitlePosition::Top)
254/// .title("Top Title");
255/// Block::bordered()
256/// .title_position(TitlePosition::Bottom)
257/// .title("Bottom Title");
258/// ```
259#[derive(Debug, Default, Display, EnumString, Clone, Copy, PartialEq, Eq, Hash)]
260pub enum TitlePosition {
261 /// Position the title at the top of the block.
262 #[default]
263 Top,
264 /// Position the title at the bottom of the block.
265 Bottom,
266}
267
268impl<'a> Block<'a> {
269 /// Creates a new block with no [`Borders`] or [`Padding`].
270 pub const fn new() -> Self {
271 Self {
272 titles: Vec::new(),
273 titles_style: Style::new(),
274 titles_alignment: Alignment::Left,
275 titles_position: TitlePosition::Top,
276 borders: Borders::NONE,
277 border_style: Style::new(),
278 border_set: BorderType::Plain.to_border_set(),
279 style: Style::new(),
280 padding: Padding::ZERO,
281 merge_borders: MergeStrategy::Replace,
282 shadow: None,
283 }
284 }
285
286 /// Create a new block with [all borders](Borders::ALL) shown
287 ///
288 /// ```
289 /// use ratatui::widgets::{Block, Borders};
290 ///
291 /// assert_eq!(Block::bordered(), Block::new().borders(Borders::ALL));
292 /// ```
293 pub const fn bordered() -> Self {
294 let mut block = Self::new();
295 block.borders = Borders::ALL;
296 block
297 }
298
299 /// Adds a title to the block using the default position.
300 ///
301 /// The position of the title is determined by the `title_position` field of the block, which
302 /// defaults to `Top`. This can be changed using the [`Block::title_position`] method. For
303 /// explicit positioning, use [`Block::title_top`] or [`Block::title_bottom`].
304 ///
305 /// The `title` function allows you to add a title to the block. You can call this function
306 /// multiple times to add multiple titles.
307 ///
308 /// Each title will be rendered with a single space separating titles that are in the same
309 /// position or alignment. When both centered and non-centered titles are rendered, the centered
310 /// space is calculated based on the full width of the block, rather than the leftover width.
311 ///
312 /// You can provide any type that can be converted into [`Line`] including: strings, string
313 /// slices (`&str`), borrowed strings (`Cow<str>`), [spans](ratatui_core::text::Span), or
314 /// vectors of [spans](ratatui_core::text::Span) (`Vec<Span>`).
315 ///
316 /// By default, the titles will avoid being rendered in the corners of the block but will align
317 /// against the left or right edge of the block if there is no border on that edge. The
318 /// following demonstrates this behavior, notice the second title is one character off to the
319 /// left.
320 ///
321 /// ```plain
322 /// ┌With at least a left border───
323 ///
324 /// Without left border───
325 /// ```
326 ///
327 /// Note: If the block is too small and multiple titles overlap, the border might get cut off at
328 /// a corner.
329 ///
330 /// # Examples
331 ///
332 /// See the [Block example] for a visual representation of how the various borders and styles
333 /// look when rendered.
334 ///
335 /// The following example demonstrates:
336 /// - Default title alignment
337 /// - Multiple titles (notice "Center" is centered according to the full with of the block, not
338 /// the leftover space)
339 /// - Two titles with the same alignment (notice the left titles are separated)
340 /// ```
341 /// use ratatui::text::Line;
342 /// use ratatui::widgets::Block;
343 ///
344 /// Block::bordered()
345 /// .title("Title")
346 /// .title(Line::from("Left").left_aligned())
347 /// .title(Line::from("Right").right_aligned())
348 /// .title(Line::from("Center").centered());
349 /// ```
350 ///
351 /// # See also
352 ///
353 /// Titles attached to a block can have default behaviors. See
354 /// - [`Block::title_style`]
355 /// - [`Block::title_alignment`]
356 ///
357 /// # History
358 ///
359 /// In previous releases of Ratatui this method accepted `Into<Title>` instead of
360 /// [`Into<Line>`]. We found that storing the position in the block and the alignment in the
361 /// line better reflects the intended use of the block and its titles. See
362 /// <https://github.com/ratatui/ratatui/issues/738> for more information.
363 ///
364 /// [Block example]: https://github.com/ratatui/ratatui/blob/main/examples/README.md#block
365 #[must_use = "method moves the value of self and returns the modified value"]
366 pub fn title<T>(mut self, title: T) -> Self
367 where
368 T: Into<Line<'a>>,
369 {
370 self.titles.push((None, title.into()));
371 self
372 }
373
374 /// Adds a title to the top of the block.
375 ///
376 /// You can provide any type that can be converted into [`Line`] including: strings, string
377 /// slices (`&str`), borrowed strings (`Cow<str>`), [spans](ratatui_core::text::Span), or
378 /// vectors of [spans](ratatui_core::text::Span) (`Vec<Span>`).
379 ///
380 /// # Example
381 ///
382 /// ```
383 /// use ratatui::{ widgets::Block, text::Line };
384 ///
385 /// Block::bordered()
386 /// .title_top("Left1") // By default in the top left corner
387 /// .title_top(Line::from("Left2").left_aligned())
388 /// .title_top(Line::from("Right").right_aligned())
389 /// .title_top(Line::from("Center").centered());
390 ///
391 /// // Renders
392 /// // ┌Left1─Left2───Center─────────Right┐
393 /// // │ │
394 /// // └──────────────────────────────────┘
395 /// ```
396 #[must_use = "method moves the value of self and returns the modified value"]
397 pub fn title_top<T: Into<Line<'a>>>(mut self, title: T) -> Self {
398 let line = title.into();
399 self.titles.push((Some(TitlePosition::Top), line));
400 self
401 }
402
403 /// Adds a title to the bottom of the block.
404 ///
405 /// You can provide any type that can be converted into [`Line`] including: strings, string
406 /// slices (`&str`), borrowed strings (`Cow<str>`), [spans](ratatui_core::text::Span), or
407 /// vectors of [spans](ratatui_core::text::Span) (`Vec<Span>`).
408 ///
409 /// # Example
410 ///
411 /// ```
412 /// use ratatui::{ widgets::Block, text::Line };
413 ///
414 /// Block::bordered()
415 /// .title_bottom("Left1") // By default in the top left corner
416 /// .title_bottom(Line::from("Left2").left_aligned())
417 /// .title_bottom(Line::from("Right").right_aligned())
418 /// .title_bottom(Line::from("Center").centered());
419 ///
420 /// // Renders
421 /// // ┌──────────────────────────────────┐
422 /// // │ │
423 /// // └Left1─Left2───Center─────────Right┘
424 /// ```
425 #[must_use = "method moves the value of self and returns the modified value"]
426 pub fn title_bottom<T: Into<Line<'a>>>(mut self, title: T) -> Self {
427 let line = title.into();
428 self.titles.push((Some(TitlePosition::Bottom), line));
429 self
430 }
431
432 /// Applies the style to all titles.
433 ///
434 /// This style will be applied to all titles of the block. If a title has a style set, it will
435 /// be applied after this style. This style will be applied after any [`Block::style`] or
436 /// [`Block::border_style`] is applied.
437 ///
438 /// See [`Style`] for more information on how merging styles works.
439 ///
440 /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
441 /// your own type that implements [`Into<Style>`]).
442 ///
443 /// [`Color`]: ratatui_core::style::Color
444 #[must_use = "method moves the value of self and returns the modified value"]
445 pub fn title_style<S: Into<Style>>(mut self, style: S) -> Self {
446 self.titles_style = style.into();
447 self
448 }
449
450 /// Sets the default [`Alignment`] for all block titles.
451 ///
452 /// Titles that explicitly set an [`Alignment`] will ignore this.
453 ///
454 /// # Example
455 ///
456 /// This example aligns all titles in the center except the "right" title which explicitly sets
457 /// [`Alignment::Right`].
458 /// ```
459 /// use ratatui::layout::Alignment;
460 /// use ratatui::text::Line;
461 /// use ratatui::widgets::Block;
462 ///
463 /// Block::bordered()
464 /// .title_alignment(Alignment::Center)
465 /// // This title won't be aligned in the center
466 /// .title(Line::from("right").right_aligned())
467 /// .title("foo")
468 /// .title("bar");
469 /// ```
470 #[must_use = "method moves the value of self and returns the modified value"]
471 pub const fn title_alignment(mut self, alignment: Alignment) -> Self {
472 self.titles_alignment = alignment;
473 self
474 }
475
476 /// Sets the default [`TitlePosition`] for all block titles.
477 ///
478 /// # Example
479 ///
480 /// This example positions all titles on the bottom by default. The "top" title explicitly sets
481 /// its position to `Top`, so it is not affected. The "foo" and "bar" titles will be positioned
482 /// at the bottom.
483 ///
484 /// ```
485 /// use ratatui::widgets::{Block, TitlePosition};
486 ///
487 /// Block::bordered()
488 /// .title_position(TitlePosition::Bottom)
489 /// .title("foo") // will be at the bottom
490 /// .title_top("top") // will be at the top
491 /// .title("bar"); // will be at the bottom
492 /// ```
493 #[must_use = "method moves the value of self and returns the modified value"]
494 pub const fn title_position(mut self, position: TitlePosition) -> Self {
495 self.titles_position = position;
496 self
497 }
498
499 /// Defines the style of the borders.
500 ///
501 /// This style is applied only to the areas covered by borders, and is applied to the block
502 /// after any [`Block::style`] is applied.
503 ///
504 /// See [`Style`] for more information on how merging styles works.
505 ///
506 /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
507 /// your own type that implements [`Into<Style>`]).
508 ///
509 /// # Example
510 ///
511 /// This example shows a `Block` with blue borders.
512 /// ```
513 /// use ratatui::style::{Style, Stylize};
514 /// use ratatui::widgets::Block;
515 /// Block::bordered().border_style(Style::new().blue());
516 /// ```
517 ///
518 /// [`Color`]: ratatui_core::style::Color
519 #[must_use = "method moves the value of self and returns the modified value"]
520 pub fn border_style<S: Into<Style>>(mut self, style: S) -> Self {
521 self.border_style = style.into();
522 self
523 }
524
525 /// Defines the style of the entire block.
526 ///
527 /// This is the most generic [`Style`] a block can receive, it will be merged with any other
528 /// more specific styles. Elements can be styled further with [`Block::title_style`] and
529 /// [`Block::border_style`], which will be applied on top of this style. If the block is used as
530 /// a container for another widget (e.g. a [`Paragraph`]), then the style of the widget is
531 /// generally applied before this style.
532 ///
533 /// See [`Style`] for more information on how merging styles works.
534 ///
535 /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
536 /// your own type that implements [`Into<Style>`]).
537 ///
538 /// # Example
539 ///
540 /// ```
541 /// use ratatui::style::{Color, Style, Stylize};
542 /// use ratatui::widgets::{Block, Paragraph};
543 ///
544 /// let block = Block::new().style(Style::new().red().on_black());
545 ///
546 /// // For border and title you can additionally apply styles on top of the block level style.
547 /// let block = Block::new()
548 /// .style(Style::new().red().bold().italic())
549 /// .border_style(Style::new().not_italic()) // will be red and bold
550 /// .title_style(Style::new().not_bold()) // will be red and italic
551 /// .title("Title");
552 ///
553 /// // To style the inner widget, you can style the widget itself.
554 /// let paragraph = Paragraph::new("Content")
555 /// .block(block)
556 /// .style(Style::new().white().not_bold()); // will be white, and italic
557 /// ```
558 ///
559 /// [`Paragraph`]: crate::paragraph::Paragraph
560 /// [`Color`]: ratatui_core::style::Color
561 #[must_use = "method moves the value of self and returns the modified value"]
562 pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
563 self.style = style.into();
564 self
565 }
566
567 /// Defines which borders to display.
568 ///
569 /// [`Borders`] can also be styled with [`Block::border_style`] and [`Block::border_type`].
570 ///
571 /// # Examples
572 ///
573 /// Display left and right borders.
574 /// ```
575 /// use ratatui::widgets::{Block, Borders};
576 /// Block::new().borders(Borders::LEFT | Borders::RIGHT);
577 /// ```
578 ///
579 /// To show all borders you can abbreviate this with [`Block::bordered`]
580 #[must_use = "method moves the value of self and returns the modified value"]
581 pub const fn borders(mut self, flag: Borders) -> Self {
582 self.borders = flag;
583 self
584 }
585
586 /// Sets the symbols used to display the border (e.g. single line, double line, thick or
587 /// rounded borders).
588 ///
589 /// Setting this overwrites any custom [`border_set`](Block::border_set) that was set.
590 ///
591 /// See [`BorderType`] for the full list of available symbols.
592 ///
593 /// # Examples
594 ///
595 /// ```
596 /// use ratatui::widgets::{Block, BorderType};
597 /// Block::bordered()
598 /// .border_type(BorderType::Rounded)
599 /// .title("Block");
600 /// // Renders
601 /// // ╭Block╮
602 /// // │ │
603 /// // ╰─────╯
604 /// ```
605 #[must_use = "method moves the value of self and returns the modified value"]
606 pub const fn border_type(mut self, border_type: BorderType) -> Self {
607 self.border_set = border_type.to_border_set();
608 self
609 }
610
611 /// Sets the symbols used to display the border as a [`ratatui_core::symbols::border::Set`].
612 ///
613 /// Setting this overwrites any [`border_type`](Block::border_type) that was set.
614 ///
615 /// # Examples
616 ///
617 /// ```
618 /// use ratatui::{widgets::Block, symbols};
619 ///
620 /// Block::bordered().border_set(symbols::border::DOUBLE).title("Block");
621 /// // Renders
622 /// // ╔Block╗
623 /// // ║ ║
624 /// // ╚═════╝
625 #[must_use = "method moves the value of self and returns the modified value"]
626 pub const fn border_set(mut self, border_set: border::Set<'a>) -> Self {
627 self.border_set = border_set;
628 self
629 }
630
631 /// Defines the padding inside a `Block`.
632 ///
633 /// See [`Padding`] for more information.
634 ///
635 /// # Examples
636 ///
637 /// This renders a `Block` with no padding (the default).
638 /// ```
639 /// use ratatui::widgets::{Block, Padding};
640 ///
641 /// Block::bordered().padding(Padding::ZERO);
642 /// // Renders
643 /// // ┌───────┐
644 /// // │content│
645 /// // └───────┘
646 /// ```
647 ///
648 /// This example shows a `Block` with padding left and right ([`Padding::horizontal`]).
649 /// Notice the two spaces before and after the content.
650 /// ```
651 /// use ratatui::widgets::{Block, Padding};
652 ///
653 /// Block::bordered().padding(Padding::horizontal(2));
654 /// // Renders
655 /// // ┌───────────┐
656 /// // │ content │
657 /// // └───────────┘
658 /// ```
659 #[must_use = "method moves the value of self and returns the modified value"]
660 pub const fn padding(mut self, padding: Padding) -> Self {
661 self.padding = padding;
662 self
663 }
664
665 /// Sets the block's [`MergeStrategy`] for overlapping characters, defaulting to [`Replace`].
666 ///
667 /// Changing the strategy to [`Exact`] or [`Fuzzy`] collapses border characters that intersect
668 /// with any previously rendered borders.
669 ///
670 /// For more information and examples, see the [collapse borders recipe] and [`MergeStrategy`]
671 /// docs.
672 ///
673 /// # Example
674 ///
675 /// ```
676 /// use ratatui::symbols::merge::MergeStrategy;
677 /// # use ratatui::widgets::{Block, BorderType};
678 ///
679 /// // Given several blocks with plain borders (1)
680 /// Block::bordered();
681 /// // and other blocks with thick borders (2) which are rendered on top of the first
682 /// Block::bordered()
683 /// .border_type(BorderType::Thick)
684 /// .merge_borders(MergeStrategy::Exact);
685 /// ```
686 ///
687 /// Rendering these blocks with `MergeStrategy::Exact` or `MergeStrategy::Fuzzy` will collapse
688 /// the borders, resulting in a clean layout without connected borders.
689 ///
690 /// ```plain
691 /// ┌───┐ ┌───┐ ┌───┲━━━┓┌───┐
692 /// │ │ │ 1 │ │ ┃ ┃│ │
693 /// │ 1 │ │ ┏━┿━┓│ 1 ┃ 2 ┃│ 1 │
694 /// │ │ │ ┃ │ ┃│ ┃ ┃│ │
695 /// └───╆━━━┓└─╂─┘ ┃└───┺━━━┛┢━━━┪
696 /// ┃ ┃ ┃ 2 ┃ ┃ ┃
697 /// ┃ 2 ┃ ┗━━━┛ ┃ 2 ┃
698 /// ┃ ┃ ┃ ┃
699 /// ┗━━━┛ ┗━━━┛
700 /// ```
701 ///
702 /// [collapse borders recipe]: https://ratatui.rs/recipes/layout/collapse-borders/
703 /// [`Replace`]: MergeStrategy::Replace
704 /// [`Exact`]: MergeStrategy::Exact
705 /// [`Fuzzy`]: MergeStrategy::Fuzzy
706 #[must_use = "method moves the value of self and returns the modified value"]
707 pub const fn merge_borders(mut self, strategy: MergeStrategy) -> Self {
708 self.merge_borders = strategy;
709 self
710 }
711
712 /// Adds a shadow behind the block.
713 ///
714 /// The shadow is rendered using the block area plus the shadow's configured offset.
715 ///
716 /// # Example
717 ///
718 /// ```
719 /// use ratatui::layout::Offset;
720 /// use ratatui::style::Stylize;
721 /// use ratatui::widgets::{Block, Shadow};
722 ///
723 /// let block = Block::bordered().title("Popup").shadow(
724 /// Shadow::dark_shade()
725 /// .black()
726 /// .on_white()
727 /// .offset(Offset::new(2, 1)),
728 /// );
729 /// ```
730 #[must_use]
731 pub fn shadow(mut self, shadow: Shadow) -> Self {
732 self.shadow = Some(shadow);
733 self
734 }
735
736 /// Computes the inner area of a block after subtracting space for borders, titles, and padding.
737 ///
738 /// # Examples
739 ///
740 /// Draw a block nested within another block
741 /// ```
742 /// use ratatui::Frame;
743 /// use ratatui::widgets::Block;
744 ///
745 /// # fn render_nested_block(frame: &mut Frame) {
746 /// let outer_block = Block::bordered().title("Outer");
747 /// let inner_block = Block::bordered().title("Inner");
748 ///
749 /// let outer_area = frame.area();
750 /// let inner_area = outer_block.inner(outer_area);
751 ///
752 /// frame.render_widget(outer_block, outer_area);
753 /// frame.render_widget(inner_block, inner_area);
754 /// # }
755 /// // Renders
756 /// // ┌Outer────────┐
757 /// // │┌Inner──────┐│
758 /// // ││ ││
759 /// // │└───────────┘│
760 /// // └─────────────┘
761 /// ```
762 pub fn inner(&self, area: Rect) -> Rect {
763 let mut inner = area;
764 if self.borders.intersects(Borders::LEFT) {
765 inner.x = inner.x.saturating_add(1).min(inner.right());
766 inner.width = inner.width.saturating_sub(1);
767 }
768 if self.borders.intersects(Borders::TOP) || self.has_title_at_position(TitlePosition::Top) {
769 inner.y = inner.y.saturating_add(1).min(inner.bottom());
770 inner.height = inner.height.saturating_sub(1);
771 }
772 if self.borders.intersects(Borders::RIGHT) {
773 inner.width = inner.width.saturating_sub(1);
774 }
775 if self.borders.intersects(Borders::BOTTOM)
776 || self.has_title_at_position(TitlePosition::Bottom)
777 {
778 inner.height = inner.height.saturating_sub(1);
779 }
780
781 inner.x = inner.x.saturating_add(self.padding.left);
782 inner.y = inner.y.saturating_add(self.padding.top);
783
784 let horizontal_padding = self.padding.left.saturating_add(self.padding.right);
785 let vertical_padding = self.padding.top.saturating_add(self.padding.bottom);
786 inner.width = inner.width.saturating_sub(horizontal_padding);
787 inner.height = inner.height.saturating_sub(vertical_padding);
788
789 inner
790 }
791
792 fn has_title_at_position(&self, position: TitlePosition) -> bool {
793 self.titles
794 .iter()
795 .any(|(pos, _)| pos.unwrap_or(self.titles_position) == position)
796 }
797}
798
799impl Widget for Block<'_> {
800 fn render(self, area: Rect, buf: &mut Buffer) {
801 Widget::render(&self, area, buf);
802 }
803}
804
805impl Widget for &Block<'_> {
806 fn render(self, area: Rect, buf: &mut Buffer) {
807 let area = area.intersection(buf.area);
808 if area.is_empty() {
809 return;
810 }
811 buf.set_style(area, self.style);
812 self.render_borders(area, buf);
813 self.render_titles(area, buf);
814 self.render_shadow(area, buf);
815 }
816}
817
818impl Block<'_> {
819 fn render_borders(&self, area: Rect, buf: &mut Buffer) {
820 self.render_sides(area, buf);
821 self.render_corners(area, buf);
822 }
823
824 fn render_sides(&self, area: Rect, buf: &mut Buffer) {
825 let left = area.left();
826 let top = area.top();
827 // area.right() and area.bottom() are outside the rect, subtract 1 to get the last row/col
828 let right = area.right().saturating_sub(1);
829 let bottom = area.bottom().saturating_sub(1);
830
831 // The first and last element of each line are not drawn when there is an adjacent line as
832 // this would cause the corner to initially be merged with a side character and then a
833 // corner character to be drawn on top of it. Some merge strategies would not produce a
834 // correct character in that case.
835 let is_replace = self.merge_borders != MergeStrategy::Replace;
836 let left_inset_amount = u16::from(is_replace && self.borders.contains(Borders::LEFT));
837 let top_inset_amount = u16::from(is_replace && self.borders.contains(Borders::TOP));
838 let right_inset_amount = u16::from(is_replace && self.borders.contains(Borders::RIGHT));
839 let bottom_inset_amount = u16::from(is_replace && self.borders.contains(Borders::BOTTOM));
840 let left_inset = left.saturating_add(left_inset_amount);
841 let top_inset = top.saturating_add(top_inset_amount);
842 let right_inset = right.saturating_sub(right_inset_amount);
843 let bottom_inset = bottom.saturating_sub(bottom_inset_amount);
844
845 let sides = [
846 (
847 Borders::LEFT,
848 left..=left,
849 top_inset..=bottom_inset,
850 self.border_set.vertical_left,
851 ),
852 (
853 Borders::TOP,
854 left_inset..=right_inset,
855 top..=top,
856 self.border_set.horizontal_top,
857 ),
858 (
859 Borders::RIGHT,
860 right..=right,
861 top_inset..=bottom_inset,
862 self.border_set.vertical_right,
863 ),
864 (
865 Borders::BOTTOM,
866 left_inset..=right_inset,
867 bottom..=bottom,
868 self.border_set.horizontal_bottom,
869 ),
870 ];
871 for (border, x_range, y_range, symbol) in sides {
872 if self.borders.contains(border) {
873 for x in x_range {
874 for y in y_range.clone() {
875 buf[(x, y)]
876 .merge_symbol(symbol, self.merge_borders)
877 .set_style(self.border_style);
878 }
879 }
880 }
881 }
882 }
883
884 fn render_corners(&self, area: Rect, buf: &mut Buffer) {
885 let corners = [
886 (
887 Borders::RIGHT | Borders::BOTTOM,
888 area.right().saturating_sub(1),
889 area.bottom().saturating_sub(1),
890 self.border_set.bottom_right,
891 ),
892 (
893 Borders::RIGHT | Borders::TOP,
894 area.right().saturating_sub(1),
895 area.top(),
896 self.border_set.top_right,
897 ),
898 (
899 Borders::LEFT | Borders::BOTTOM,
900 area.left(),
901 area.bottom().saturating_sub(1),
902 self.border_set.bottom_left,
903 ),
904 (
905 Borders::LEFT | Borders::TOP,
906 area.left(),
907 area.top(),
908 self.border_set.top_left,
909 ),
910 ];
911
912 for (border, x, y, symbol) in corners {
913 if self.borders.contains(border) {
914 buf[(x, y)]
915 .merge_symbol(symbol, self.merge_borders)
916 .set_style(self.border_style);
917 }
918 }
919 }
920 fn render_titles(&self, area: Rect, buf: &mut Buffer) {
921 self.render_title_position(TitlePosition::Top, area, buf);
922 self.render_title_position(TitlePosition::Bottom, area, buf);
923 }
924
925 fn render_title_position(&self, position: TitlePosition, area: Rect, buf: &mut Buffer) {
926 // NOTE: the order in which these functions are called defines the overlapping behavior
927 self.render_left_titles(position, area, buf);
928 self.render_center_titles(position, area, buf);
929 self.render_right_titles(position, area, buf);
930 }
931
932 /// Render titles aligned to the right of the block
933 ///
934 /// Currently (due to the way lines are truncated), the right side of the leftmost title will
935 /// be cut off if the block is too small to fit all titles. This is not ideal and should be
936 /// the left side of that leftmost that is cut off. This is due to the line being truncated
937 /// incorrectly. See <https://github.com/ratatui/ratatui/issues/932>
938 #[expect(clippy::similar_names)]
939 fn render_right_titles(&self, position: TitlePosition, area: Rect, buf: &mut Buffer) {
940 let titles = self.filtered_titles(position, Alignment::Right);
941 let mut titles_area = self.titles_area(area, position);
942
943 // render titles in reverse order to align them to the right
944 for title in titles.rev() {
945 if titles_area.is_empty() {
946 break;
947 }
948 let title_width = Self::line_width_u16(title);
949 let title_area = Rect {
950 x: titles_area
951 .right()
952 .saturating_sub(title_width)
953 .max(titles_area.left()),
954 width: title_width.min(titles_area.width),
955 ..titles_area
956 };
957 buf.set_style(title_area, self.titles_style);
958 title.render(title_area, buf);
959
960 // bump the width of the titles area to the left
961 titles_area.width = titles_area
962 .width
963 .saturating_sub(title_width)
964 .saturating_sub(1); // space between titles
965 }
966 }
967
968 /// Render titles in the center of the block
969 fn render_center_titles(&self, position: TitlePosition, area: Rect, buf: &mut Buffer) {
970 let area = self.titles_area(area, position);
971 let titles = self
972 .filtered_titles(position, Alignment::Center)
973 .collect_vec();
974 // titles are rendered with a space after each title except the last one
975 let total_width = titles
976 .iter()
977 .map(|title| Self::line_width_u16(title).saturating_add(1))
978 .fold(0, u16::saturating_add)
979 .saturating_sub(1);
980
981 if total_width <= area.width {
982 self.render_centered_titles_without_truncation(titles, total_width, area, buf);
983 } else {
984 self.render_centered_titles_with_truncation(titles, total_width, area, buf);
985 }
986 }
987
988 fn render_centered_titles_without_truncation(
989 &self,
990 titles: Vec<&Line<'_>>,
991 total_width: u16,
992 area: Rect,
993 buf: &mut Buffer,
994 ) {
995 // titles fit in the area, center them
996 let x = area
997 .left()
998 .saturating_add(area.width.saturating_sub(total_width) / 2);
999 let mut area = Rect { x, ..area };
1000 for title in titles {
1001 let width = Self::line_width_u16(title);
1002 let title_area = Rect { width, ..area };
1003 buf.set_style(title_area, self.titles_style);
1004 title.render(title_area, buf);
1005 // Move the rendering cursor to the right, leaving 1 column space.
1006 let advance = width.saturating_add(1);
1007 area.x = area.x.saturating_add(advance);
1008 area.width = area.width.saturating_sub(advance);
1009 }
1010 }
1011
1012 fn render_centered_titles_with_truncation(
1013 &self,
1014 titles: Vec<&Line<'_>>,
1015 total_width: u16,
1016 mut area: Rect,
1017 buf: &mut Buffer,
1018 ) {
1019 // titles do not fit in the area, truncate the left side using an offset. The right side
1020 // is truncated by the area width.
1021 let mut offset = total_width.saturating_sub(area.width) / 2;
1022 for title in titles {
1023 if area.is_empty() {
1024 break;
1025 }
1026 let width = area
1027 .width
1028 .min(Self::line_width_u16(title))
1029 .saturating_sub(offset);
1030 let title_area = Rect { width, ..area };
1031 buf.set_style(title_area, self.titles_style);
1032 if offset > 0 {
1033 // truncate the left side of the title to fit the area
1034 title.clone().right_aligned().render(title_area, buf);
1035 offset = offset.saturating_sub(width).saturating_sub(1);
1036 } else {
1037 // truncate the right side of the title to fit the area if needed
1038 title.clone().left_aligned().render(title_area, buf);
1039 }
1040 // Leave 1 column of spacing between titles.
1041 let advance = width.saturating_add(1);
1042 area.x = area.x.saturating_add(advance);
1043 area.width = area.width.saturating_sub(advance);
1044 }
1045 }
1046
1047 /// Render titles aligned to the left of the block
1048 #[expect(clippy::similar_names)]
1049 fn render_left_titles(&self, position: TitlePosition, area: Rect, buf: &mut Buffer) {
1050 let titles = self.filtered_titles(position, Alignment::Left);
1051 let mut titles_area = self.titles_area(area, position);
1052 for title in titles {
1053 if titles_area.is_empty() {
1054 break;
1055 }
1056 let title_width = Self::line_width_u16(title);
1057 let title_area = Rect {
1058 width: title_width.min(titles_area.width),
1059 ..titles_area
1060 };
1061 buf.set_style(title_area, self.titles_style);
1062 title.render(title_area, buf);
1063
1064 // bump the titles area to the right and reduce its width
1065 let advance = title_width.saturating_add(1);
1066 titles_area.x = titles_area.x.saturating_add(advance);
1067 titles_area.width = titles_area.width.saturating_sub(advance);
1068 }
1069 }
1070
1071 fn render_shadow(&self, base_area: Rect, buf: &mut Buffer) {
1072 if let Some(shadow) = &self.shadow {
1073 shadow.render(base_area, buf);
1074 }
1075 }
1076
1077 /// An iterator over the titles that match the position and alignment
1078 fn filtered_titles(
1079 &self,
1080 position: TitlePosition,
1081 alignment: Alignment,
1082 ) -> impl DoubleEndedIterator<Item = &Line<'_>> {
1083 self.titles
1084 .iter()
1085 .filter(move |(pos, _)| pos.unwrap_or(self.titles_position) == position)
1086 .filter(move |(_, line)| line.alignment.unwrap_or(self.titles_alignment) == alignment)
1087 .map(|(_, line)| line)
1088 }
1089
1090 /// Return the rendered line width clamped to `u16` for layout arithmetic.
1091 fn line_width_u16(line: &Line<'_>) -> u16 {
1092 line.width().min(u16::MAX as usize) as u16
1093 }
1094
1095 /// An area that is one line tall and spans the width of the block excluding the borders and
1096 /// is positioned at the top or bottom of the block.
1097 fn titles_area(&self, area: Rect, position: TitlePosition) -> Rect {
1098 let left_border = u16::from(self.borders.contains(Borders::LEFT));
1099 let right_border = u16::from(self.borders.contains(Borders::RIGHT));
1100 Rect {
1101 x: area.left().saturating_add(left_border),
1102 y: match position {
1103 TitlePosition::Top => area.top(),
1104 TitlePosition::Bottom => area.bottom().saturating_sub(1),
1105 },
1106 width: area
1107 .width
1108 .saturating_sub(left_border)
1109 .saturating_sub(right_border),
1110 height: 1,
1111 }
1112 }
1113
1114 /// Calculate the left, and right space the [`Block`] will take up.
1115 ///
1116 /// The result takes the [`Block`]'s, [`Borders`], and [`Padding`] into account.
1117 pub(crate) fn horizontal_space(&self) -> (u16, u16) {
1118 let left = self
1119 .padding
1120 .left
1121 .saturating_add(u16::from(self.borders.contains(Borders::LEFT)));
1122 let right = self
1123 .padding
1124 .right
1125 .saturating_add(u16::from(self.borders.contains(Borders::RIGHT)));
1126 (left, right)
1127 }
1128
1129 /// Calculate the top, and bottom space that the [`Block`] will take up.
1130 ///
1131 /// Takes the [`Padding`], [`TitlePosition`], and the [`Borders`] that are selected into
1132 /// account when calculating the result.
1133 pub(crate) fn vertical_space(&self) -> (u16, u16) {
1134 let has_top =
1135 self.borders.contains(Borders::TOP) || self.has_title_at_position(TitlePosition::Top);
1136 let top = self.padding.top.saturating_add(u16::from(has_top));
1137 let has_bottom = self.borders.contains(Borders::BOTTOM)
1138 || self.has_title_at_position(TitlePosition::Bottom);
1139 let bottom = self.padding.bottom.saturating_add(u16::from(has_bottom));
1140 (top, bottom)
1141 }
1142}
1143
1144/// An extension trait for [`Block`] that provides some convenience methods.
1145///
1146/// This is implemented for [`Option<Block>`](Option) to simplify the common case of having a
1147/// widget with an optional block.
1148pub trait BlockExt {
1149 /// Return the inner area of the block if it is `Some`. Otherwise, returns `area`.
1150 ///
1151 /// This is a useful convenience method for widgets that have an `Option<Block>` field
1152 fn inner_if_some(&self, area: Rect) -> Rect;
1153}
1154
1155impl BlockExt for Option<Block<'_>> {
1156 fn inner_if_some(&self, area: Rect) -> Rect {
1157 self.as_ref().map_or(area, |block| block.inner(area))
1158 }
1159}
1160
1161impl Styled for Block<'_> {
1162 type Item = Self;
1163
1164 fn style(&self) -> Style {
1165 self.style
1166 }
1167
1168 fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
1169 self.style(style)
1170 }
1171}
1172
1173#[cfg(test)]
1174mod tests {
1175 use alloc::{format, vec};
1176
1177 use itertools::iproduct;
1178 use ratatui_core::layout::Offset;
1179 use ratatui_core::style::{Color, Modifier, Stylize};
1180 use rstest::rstest;
1181 use strum::ParseError;
1182
1183 use super::*;
1184
1185 #[test]
1186 fn create_with_all_borders() {
1187 let block = Block::bordered();
1188 assert_eq!(block.borders, Borders::all());
1189 }
1190
1191 #[rstest]
1192 #[case::none_0(Borders::NONE, Rect::ZERO, Rect::ZERO)]
1193 #[case::none_1(Borders::NONE, Rect::new(0, 0, 1, 1), Rect::new(0, 0, 1, 1))]
1194 #[case::left_0(Borders::LEFT, Rect::ZERO, Rect::ZERO)]
1195 #[case::left_w1(Borders::LEFT, Rect::new(0, 0, 0, 1), Rect::new(0, 0, 0, 1))]
1196 #[case::left_w2(Borders::LEFT, Rect::new(0, 0, 1, 1), Rect::new(1, 0, 0, 1))]
1197 #[case::left_w3(Borders::LEFT, Rect::new(0, 0, 2, 1), Rect::new(1, 0, 1, 1))]
1198 #[case::top_0(Borders::TOP, Rect::ZERO, Rect::ZERO)]
1199 #[case::top_h1(Borders::TOP, Rect::new(0, 0, 1, 0), Rect::new(0, 0, 1, 0))]
1200 #[case::top_h2(Borders::TOP, Rect::new(0, 0, 1, 1), Rect::new(0, 1, 1, 0))]
1201 #[case::top_h3(Borders::TOP, Rect::new(0, 0, 1, 2), Rect::new(0, 1, 1, 1))]
1202 #[case::right_0(Borders::RIGHT, Rect::ZERO, Rect::ZERO)]
1203 #[case::right_w1(Borders::RIGHT, Rect::new(0, 0, 0, 1), Rect::new(0, 0, 0, 1))]
1204 #[case::right_w2(Borders::RIGHT, Rect::new(0, 0, 1, 1), Rect::new(0, 0, 0, 1))]
1205 #[case::right_w3(Borders::RIGHT, Rect::new(0, 0, 2, 1), Rect::new(0, 0, 1, 1))]
1206 #[case::bottom_0(Borders::BOTTOM, Rect::ZERO, Rect::ZERO)]
1207 #[case::bottom_h1(Borders::BOTTOM, Rect::new(0, 0, 1, 0), Rect::new(0, 0, 1, 0))]
1208 #[case::bottom_h2(Borders::BOTTOM, Rect::new(0, 0, 1, 1), Rect::new(0, 0, 1, 0))]
1209 #[case::bottom_h3(Borders::BOTTOM, Rect::new(0, 0, 1, 2), Rect::new(0, 0, 1, 1))]
1210 #[case::all_0(Borders::ALL, Rect::ZERO, Rect::ZERO)]
1211 #[case::all_1(Borders::ALL, Rect::new(0, 0, 1, 1), Rect::new(1, 1, 0, 0))]
1212 #[case::all_2(Borders::ALL, Rect::new(0, 0, 2, 2), Rect::new(1, 1, 0, 0))]
1213 #[case::all_3(Borders::ALL, Rect::new(0, 0, 3, 3), Rect::new(1, 1, 1, 1))]
1214 fn inner_takes_into_account_the_borders(
1215 #[case] borders: Borders,
1216 #[case] area: Rect,
1217 #[case] expected: Rect,
1218 ) {
1219 let block = Block::new().borders(borders);
1220 assert_eq!(block.inner(area), expected);
1221 }
1222
1223 #[rstest]
1224 #[case::left(Alignment::Left)]
1225 #[case::center(Alignment::Center)]
1226 #[case::right(Alignment::Right)]
1227 fn inner_takes_into_account_the_title(#[case] alignment: Alignment) {
1228 let area = Rect::new(0, 0, 0, 1);
1229 let expected = Rect::new(0, 1, 0, 0);
1230
1231 let block = Block::new().title(Line::from("Test").alignment(alignment));
1232 assert_eq!(block.inner(area), expected);
1233 }
1234
1235 #[rstest]
1236 #[case::top_top(Block::new().title_top("Test").borders(Borders::TOP), Rect::new(0, 1, 0, 1))]
1237 #[case::top_bot(Block::new().title_top("Test").borders(Borders::BOTTOM), Rect::new(0, 1, 0, 0))]
1238 #[case::bot_top(Block::new().title_bottom("Test").borders(Borders::TOP), Rect::new(0, 1, 0, 0))]
1239 #[case::bot_bot(Block::new().title_bottom("Test").borders(Borders::BOTTOM), Rect::new(0, 0, 0, 1))]
1240 fn inner_takes_into_account_border_and_title(#[case] block: Block, #[case] expected: Rect) {
1241 let area = Rect::new(0, 0, 0, 2);
1242 assert_eq!(block.inner(area), expected);
1243 }
1244
1245 #[test]
1246 fn has_title_at_position_takes_into_account_all_positioning_declarations() {
1247 let block = Block::new();
1248 assert!(!block.has_title_at_position(TitlePosition::Top));
1249 assert!(!block.has_title_at_position(TitlePosition::Bottom));
1250
1251 let block = Block::new().title_top("test");
1252 assert!(block.has_title_at_position(TitlePosition::Top));
1253 assert!(!block.has_title_at_position(TitlePosition::Bottom));
1254
1255 let block = Block::new().title_bottom("test");
1256 assert!(!block.has_title_at_position(TitlePosition::Top));
1257 assert!(block.has_title_at_position(TitlePosition::Bottom));
1258
1259 let block = Block::new().title_top("test").title_bottom("test");
1260 assert!(block.has_title_at_position(TitlePosition::Top));
1261 assert!(block.has_title_at_position(TitlePosition::Bottom));
1262 }
1263
1264 #[rstest]
1265 #[case::none(Borders::NONE, (0, 0))]
1266 #[case::top(Borders::TOP, (1, 0))]
1267 #[case::right(Borders::RIGHT, (0, 0))]
1268 #[case::bottom(Borders::BOTTOM, (0, 1))]
1269 #[case::left(Borders::LEFT, (0, 0))]
1270 #[case::top_right(Borders::TOP | Borders::RIGHT, (1, 0))]
1271 #[case::top_bottom(Borders::TOP | Borders::BOTTOM, (1, 1))]
1272 #[case::top_left(Borders::TOP | Borders::LEFT, (1, 0))]
1273 #[case::bottom_right(Borders::BOTTOM | Borders::RIGHT, (0, 1))]
1274 #[case::bottom_left(Borders::BOTTOM | Borders::LEFT, (0, 1))]
1275 #[case::left_right(Borders::LEFT | Borders::RIGHT, (0, 0))]
1276 fn vertical_space_takes_into_account_borders(
1277 #[case] borders: Borders,
1278 #[case] vertical_space: (u16, u16),
1279 ) {
1280 let block = Block::new().borders(borders);
1281 assert_eq!(block.vertical_space(), vertical_space);
1282 }
1283
1284 #[rstest]
1285 #[case::top_border_top_p1(Borders::TOP, Padding::new(0, 0, 1, 0), (2, 0))]
1286 #[case::right_border_top_p1(Borders::RIGHT, Padding::new(0, 0, 1, 0), (1, 0))]
1287 #[case::bottom_border_top_p1(Borders::BOTTOM, Padding::new(0, 0, 1, 0), (1, 1))]
1288 #[case::left_border_top_p1(Borders::LEFT, Padding::new(0, 0, 1, 0), (1, 0))]
1289 #[case::top_bottom_border_all_p3(Borders::TOP | Borders::BOTTOM, Padding::new(100, 100, 4, 5), (5, 6))]
1290 #[case::no_border(Borders::NONE, Padding::new(100, 100, 10, 13), (10, 13))]
1291 #[case::all(Borders::ALL, Padding::new(100, 100, 1, 3), (2, 4))]
1292 fn vertical_space_takes_into_account_padding(
1293 #[case] borders: Borders,
1294 #[case] padding: Padding,
1295 #[case] vertical_space: (u16, u16),
1296 ) {
1297 let block = Block::new().borders(borders).padding(padding);
1298 assert_eq!(block.vertical_space(), vertical_space);
1299 }
1300
1301 #[test]
1302 fn vertical_space_takes_into_account_titles() {
1303 let block = Block::new().title_top("Test");
1304 assert_eq!(block.vertical_space(), (1, 0));
1305
1306 let block = Block::new().title_bottom("Test");
1307 assert_eq!(block.vertical_space(), (0, 1));
1308 }
1309
1310 #[rstest]
1311 #[case::top_border_top_title(Block::new(), Borders::TOP, TitlePosition::Top, (1, 0))]
1312 #[case::right_border_top_title(Block::new(), Borders::RIGHT, TitlePosition::Top, (1, 0))]
1313 #[case::bottom_border_top_title(Block::new(), Borders::BOTTOM, TitlePosition::Top, (1, 1))]
1314 #[case::left_border_top_title(Block::new(), Borders::LEFT, TitlePosition::Top, (1, 0))]
1315 #[case::top_border_top_title(Block::new(), Borders::TOP, TitlePosition::Bottom, (1, 1))]
1316 #[case::right_border_top_title(Block::new(), Borders::RIGHT, TitlePosition::Bottom, (0, 1))]
1317 #[case::bottom_border_top_title(Block::new(), Borders::BOTTOM, TitlePosition::Bottom, (0, 1))]
1318 #[case::left_border_top_title(Block::new(), Borders::LEFT, TitlePosition::Bottom, (0, 1))]
1319 fn vertical_space_takes_into_account_borders_and_title(
1320 #[case] block: Block,
1321 #[case] borders: Borders,
1322 #[case] pos: TitlePosition,
1323 #[case] vertical_space: (u16, u16),
1324 ) {
1325 let block = block.borders(borders).title_position(pos).title("Test");
1326 assert_eq!(block.vertical_space(), vertical_space);
1327 }
1328
1329 #[test]
1330 fn horizontal_space_takes_into_account_borders() {
1331 let block = Block::bordered();
1332 assert_eq!(block.horizontal_space(), (1, 1));
1333
1334 let block = Block::new().borders(Borders::LEFT);
1335 assert_eq!(block.horizontal_space(), (1, 0));
1336
1337 let block = Block::new().borders(Borders::RIGHT);
1338 assert_eq!(block.horizontal_space(), (0, 1));
1339 }
1340
1341 #[test]
1342 fn horizontal_space_takes_into_account_padding() {
1343 let block = Block::new().padding(Padding::new(1, 1, 100, 100));
1344 assert_eq!(block.horizontal_space(), (1, 1));
1345
1346 let block = Block::new().padding(Padding::new(3, 5, 0, 0));
1347 assert_eq!(block.horizontal_space(), (3, 5));
1348
1349 let block = Block::new().padding(Padding::new(0, 1, 100, 100));
1350 assert_eq!(block.horizontal_space(), (0, 1));
1351
1352 let block = Block::new().padding(Padding::new(1, 0, 100, 100));
1353 assert_eq!(block.horizontal_space(), (1, 0));
1354 }
1355
1356 #[rstest]
1357 #[case::all_bordered_all_padded(Block::bordered(), Padding::new(1, 1, 1, 1), (2, 2))]
1358 #[case::all_bordered_left_padded(Block::bordered(), Padding::new(1, 0, 0, 0), (2, 1))]
1359 #[case::all_bordered_right_padded(Block::bordered(), Padding::new(0, 1, 0, 0), (1, 2))]
1360 #[case::all_bordered_top_padded(Block::bordered(), Padding::new(0, 0, 1, 0), (1, 1))]
1361 #[case::all_bordered_bottom_padded(Block::bordered(), Padding::new(0, 0, 0, 1), (1, 1))]
1362 #[case::left_bordered_left_padded(Block::new().borders(Borders::LEFT), Padding::new(1, 0, 0, 0), (2, 0))]
1363 #[case::left_bordered_right_padded(Block::new().borders(Borders::LEFT), Padding::new(0, 1, 0, 0), (1, 1))]
1364 #[case::right_bordered_right_padded(Block::new().borders(Borders::RIGHT), Padding::new(0, 1, 0, 0), (0, 2))]
1365 #[case::right_bordered_left_padded(Block::new().borders(Borders::RIGHT), Padding::new(1, 0, 0, 0), (1, 1))]
1366 fn horizontal_space_takes_into_account_borders_and_padding(
1367 #[case] block: Block,
1368 #[case] padding: Padding,
1369 #[case] horizontal_space: (u16, u16),
1370 ) {
1371 let block = block.padding(padding);
1372 assert_eq!(block.horizontal_space(), horizontal_space);
1373 }
1374
1375 #[test]
1376 const fn border_type_can_be_const() {
1377 const _PLAIN: border::Set = BorderType::border_symbols(BorderType::Plain);
1378 }
1379
1380 #[test]
1381 fn block_new() {
1382 assert_eq!(
1383 Block::new(),
1384 Block {
1385 titles: Vec::new(),
1386 titles_style: Style::new(),
1387 titles_alignment: Alignment::Left,
1388 titles_position: TitlePosition::Top,
1389 borders: Borders::NONE,
1390 border_style: Style::new(),
1391 border_set: BorderType::Plain.to_border_set(),
1392 style: Style::new(),
1393 padding: Padding::ZERO,
1394 merge_borders: MergeStrategy::Replace,
1395 shadow: None,
1396 }
1397 );
1398 }
1399
1400 #[test]
1401 const fn block_can_be_const() {
1402 const _DEFAULT_STYLE: Style = Style::new();
1403 const _DEFAULT_PADDING: Padding = Padding::uniform(1);
1404 const _DEFAULT_BLOCK: Block = Block::bordered()
1405 // the following methods are no longer const because they use Into<Style>
1406 // .style(_DEFAULT_STYLE) // no longer const
1407 // .border_style(_DEFAULT_STYLE) // no longer const
1408 // .title_style(_DEFAULT_STYLE) // no longer const
1409 .title_alignment(Alignment::Left)
1410 .title_position(TitlePosition::Top)
1411 .padding(_DEFAULT_PADDING);
1412 }
1413
1414 /// Ensure Style from/into works the way a user would use it.
1415 #[test]
1416 fn style_into_works_from_user_view() {
1417 // nominal style
1418 let block = Block::new().style(Style::new().red());
1419 assert_eq!(block.style, Style::new().red());
1420
1421 // auto-convert from Color
1422 let block = Block::new().style(Color::Red);
1423 assert_eq!(block.style, Style::new().red());
1424
1425 // auto-convert from (Color, Color)
1426 let block = Block::new().style((Color::Red, Color::Blue));
1427 assert_eq!(block.style, Style::new().red().on_blue());
1428
1429 // auto-convert from Modifier
1430 let block = Block::new().style(Modifier::BOLD | Modifier::ITALIC);
1431 assert_eq!(block.style, Style::new().bold().italic());
1432
1433 // auto-convert from (Modifier, Modifier)
1434 let block = Block::new().style((Modifier::BOLD | Modifier::ITALIC, Modifier::DIM));
1435 assert_eq!(block.style, Style::new().bold().italic().not_dim());
1436
1437 // auto-convert from (Color, Modifier)
1438 let block = Block::new().style((Color::Red, Modifier::BOLD));
1439 assert_eq!(block.style, Style::new().red().bold());
1440
1441 // auto-convert from (Color, Color, Modifier)
1442 let block = Block::new().style((Color::Red, Color::Blue, Modifier::BOLD));
1443 assert_eq!(block.style, Style::new().red().on_blue().bold());
1444
1445 // auto-convert from (Color, Color, Modifier, Modifier)
1446 let block = Block::new().style((
1447 Color::Red,
1448 Color::Blue,
1449 Modifier::BOLD | Modifier::ITALIC,
1450 Modifier::DIM,
1451 ));
1452 assert_eq!(
1453 block.style,
1454 Style::new().red().on_blue().bold().italic().not_dim()
1455 );
1456 }
1457
1458 #[test]
1459 fn can_be_stylized() {
1460 let block = Block::new().black().on_white().bold().not_dim();
1461 assert_eq!(
1462 block.style,
1463 Style::default()
1464 .fg(Color::Black)
1465 .bg(Color::White)
1466 .add_modifier(Modifier::BOLD)
1467 .remove_modifier(Modifier::DIM)
1468 );
1469 }
1470
1471 #[test]
1472 fn title_top_bottom() {
1473 let mut buffer = Buffer::empty(Rect::new(0, 0, 11, 3));
1474 Block::bordered()
1475 .title_top(Line::raw("A").left_aligned())
1476 .title_top(Line::raw("B").centered())
1477 .title_top(Line::raw("C").right_aligned())
1478 .title_bottom(Line::raw("D").left_aligned())
1479 .title_bottom(Line::raw("E").centered())
1480 .title_bottom(Line::raw("F").right_aligned())
1481 .render(buffer.area, &mut buffer);
1482 #[rustfmt::skip]
1483 let expected = Buffer::with_lines([
1484 "┌A───B───C┐",
1485 "│ │",
1486 "└D───E───F┘",
1487 ]);
1488 assert_eq!(buffer, expected);
1489 }
1490
1491 #[test]
1492 fn title_alignment() {
1493 let tests = vec![
1494 (Alignment::Left, "test "),
1495 (Alignment::Center, " test "),
1496 (Alignment::Right, " test"),
1497 ];
1498 for (alignment, expected) in tests {
1499 let mut buffer = Buffer::empty(Rect::new(0, 0, 8, 1));
1500 Block::new()
1501 .title_alignment(alignment)
1502 .title("test")
1503 .render(buffer.area, &mut buffer);
1504 assert_eq!(buffer, Buffer::with_lines([expected]));
1505 }
1506 }
1507
1508 #[test]
1509 fn title_alignment_overrides_block_title_alignment() {
1510 let tests = vec![
1511 (Alignment::Right, Alignment::Left, "test "),
1512 (Alignment::Left, Alignment::Center, " test "),
1513 (Alignment::Center, Alignment::Right, " test"),
1514 ];
1515 for (block_title_alignment, alignment, expected) in tests {
1516 let mut buffer = Buffer::empty(Rect::new(0, 0, 8, 1));
1517 Block::new()
1518 .title_alignment(block_title_alignment)
1519 .title(Line::from("test").alignment(alignment))
1520 .render(buffer.area, &mut buffer);
1521 assert_eq!(buffer, Buffer::with_lines([expected]));
1522 }
1523 }
1524
1525 /// This is a regression test for bug <https://github.com/ratatui/ratatui/issues/929>
1526 #[test]
1527 fn render_right_aligned_empty_title() {
1528 let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
1529 Block::new()
1530 .title_alignment(Alignment::Right)
1531 .title("")
1532 .render(buffer.area, &mut buffer);
1533 assert_eq!(buffer, Buffer::with_lines([" "; 3]));
1534 }
1535
1536 #[test]
1537 fn title_position() {
1538 let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 2));
1539 Block::new()
1540 .title_position(TitlePosition::Bottom)
1541 .title("test")
1542 .render(buffer.area, &mut buffer);
1543 assert_eq!(buffer, Buffer::with_lines([" ", "test"]));
1544 }
1545
1546 #[test]
1547 fn title_content_style() {
1548 for alignment in [Alignment::Left, Alignment::Center, Alignment::Right] {
1549 let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 1));
1550 Block::new()
1551 .title_alignment(alignment)
1552 .title("test".yellow())
1553 .render(buffer.area, &mut buffer);
1554 assert_eq!(buffer, Buffer::with_lines(["test".yellow()]));
1555 }
1556 }
1557
1558 #[test]
1559 fn block_title_style() {
1560 for alignment in [Alignment::Left, Alignment::Center, Alignment::Right] {
1561 let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 1));
1562 Block::new()
1563 .title_alignment(alignment)
1564 .title_style(Style::new().yellow())
1565 .title("test")
1566 .render(buffer.area, &mut buffer);
1567 assert_eq!(buffer, Buffer::with_lines(["test".yellow()]));
1568 }
1569 }
1570
1571 #[test]
1572 fn title_style_overrides_block_title_style() {
1573 for alignment in [Alignment::Left, Alignment::Center, Alignment::Right] {
1574 let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 1));
1575 Block::new()
1576 .title_alignment(alignment)
1577 .title_style(Style::new().green().on_red())
1578 .title("test".yellow())
1579 .render(buffer.area, &mut buffer);
1580 assert_eq!(buffer, Buffer::with_lines(["test".yellow().on_red()]));
1581 }
1582 }
1583
1584 #[test]
1585 fn title_border_style() {
1586 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
1587 Block::bordered()
1588 .title("test")
1589 .border_style(Style::new().yellow())
1590 .render(buffer.area, &mut buffer);
1591 #[rustfmt::skip]
1592 let mut expected = Buffer::with_lines([
1593 "┌test────┐",
1594 "│ │",
1595 "└────────┘",
1596 ]);
1597 expected.set_style(Rect::new(0, 0, 10, 3), Style::new().yellow());
1598 expected.set_style(Rect::new(1, 1, 8, 1), Style::reset());
1599 assert_eq!(buffer, expected);
1600 }
1601
1602 #[test]
1603 fn border_type_to_string() {
1604 assert_eq!(format!("{}", BorderType::Plain), "Plain");
1605 assert_eq!(format!("{}", BorderType::Rounded), "Rounded");
1606 assert_eq!(format!("{}", BorderType::Double), "Double");
1607 assert_eq!(format!("{}", BorderType::Thick), "Thick");
1608 assert_eq!(
1609 format!("{}", BorderType::LightDoubleDashed),
1610 "LightDoubleDashed"
1611 );
1612 assert_eq!(
1613 format!("{}", BorderType::HeavyDoubleDashed),
1614 "HeavyDoubleDashed"
1615 );
1616 assert_eq!(
1617 format!("{}", BorderType::LightTripleDashed),
1618 "LightTripleDashed"
1619 );
1620 assert_eq!(
1621 format!("{}", BorderType::HeavyTripleDashed),
1622 "HeavyTripleDashed"
1623 );
1624 assert_eq!(
1625 format!("{}", BorderType::LightQuadrupleDashed),
1626 "LightQuadrupleDashed"
1627 );
1628 assert_eq!(
1629 format!("{}", BorderType::HeavyQuadrupleDashed),
1630 "HeavyQuadrupleDashed"
1631 );
1632 }
1633
1634 #[test]
1635 fn border_type_from_str() {
1636 assert_eq!("Plain".parse(), Ok(BorderType::Plain));
1637 assert_eq!("Rounded".parse(), Ok(BorderType::Rounded));
1638 assert_eq!("Double".parse(), Ok(BorderType::Double));
1639 assert_eq!("Thick".parse(), Ok(BorderType::Thick));
1640 assert_eq!(
1641 "LightDoubleDashed".parse(),
1642 Ok(BorderType::LightDoubleDashed)
1643 );
1644 assert_eq!(
1645 "HeavyDoubleDashed".parse(),
1646 Ok(BorderType::HeavyDoubleDashed)
1647 );
1648 assert_eq!(
1649 "LightTripleDashed".parse(),
1650 Ok(BorderType::LightTripleDashed)
1651 );
1652 assert_eq!(
1653 "HeavyTripleDashed".parse(),
1654 Ok(BorderType::HeavyTripleDashed)
1655 );
1656 assert_eq!(
1657 "LightQuadrupleDashed".parse(),
1658 Ok(BorderType::LightQuadrupleDashed)
1659 );
1660 assert_eq!(
1661 "HeavyQuadrupleDashed".parse(),
1662 Ok(BorderType::HeavyQuadrupleDashed)
1663 );
1664 assert_eq!("".parse::<BorderType>(), Err(ParseError::VariantNotFound));
1665 }
1666
1667 #[test]
1668 fn render_plain_border() {
1669 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
1670 Block::bordered()
1671 .border_type(BorderType::Plain)
1672 .render(buffer.area, &mut buffer);
1673 #[rustfmt::skip]
1674 let expected = Buffer::with_lines([
1675 "┌────────┐",
1676 "│ │",
1677 "└────────┘",
1678 ]);
1679 assert_eq!(buffer, expected);
1680 }
1681
1682 #[test]
1683 fn render_rounded_border() {
1684 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
1685 Block::bordered()
1686 .border_type(BorderType::Rounded)
1687 .render(buffer.area, &mut buffer);
1688 #[rustfmt::skip]
1689 let expected = Buffer::with_lines([
1690 "╭────────╮",
1691 "│ │",
1692 "╰────────╯",
1693 ]);
1694 assert_eq!(buffer, expected);
1695 }
1696
1697 #[test]
1698 fn render_double_border() {
1699 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
1700 Block::bordered()
1701 .border_type(BorderType::Double)
1702 .render(buffer.area, &mut buffer);
1703 #[rustfmt::skip]
1704 let expected = Buffer::with_lines([
1705 "╔════════╗",
1706 "║ ║",
1707 "╚════════╝",
1708 ]);
1709 assert_eq!(buffer, expected);
1710 }
1711
1712 #[test]
1713 fn render_quadrant_inside() {
1714 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
1715 Block::bordered()
1716 .border_type(BorderType::QuadrantInside)
1717 .render(buffer.area, &mut buffer);
1718 #[rustfmt::skip]
1719 let expected = Buffer::with_lines([
1720 "▗▄▄▄▄▄▄▄▄▖",
1721 "▐ ▌",
1722 "▝▀▀▀▀▀▀▀▀▘",
1723 ]);
1724 assert_eq!(buffer, expected);
1725 }
1726
1727 #[test]
1728 fn render_border_quadrant_outside() {
1729 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
1730 Block::bordered()
1731 .border_type(BorderType::QuadrantOutside)
1732 .render(buffer.area, &mut buffer);
1733 #[rustfmt::skip]
1734 let expected = Buffer::with_lines([
1735 "▛▀▀▀▀▀▀▀▀▜",
1736 "▌ ▐",
1737 "▙▄▄▄▄▄▄▄▄▟",
1738 ]);
1739 assert_eq!(buffer, expected);
1740 }
1741
1742 #[test]
1743 fn render_solid_border() {
1744 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
1745 Block::bordered()
1746 .border_type(BorderType::Thick)
1747 .render(buffer.area, &mut buffer);
1748 #[rustfmt::skip]
1749 let expected = Buffer::with_lines([
1750 "┏━━━━━━━━┓",
1751 "┃ ┃",
1752 "┗━━━━━━━━┛",
1753 ]);
1754 assert_eq!(buffer, expected);
1755 }
1756
1757 #[test]
1758 fn render_light_double_dashed_border() {
1759 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
1760 Block::bordered()
1761 .border_type(BorderType::LightDoubleDashed)
1762 .render(buffer.area, &mut buffer);
1763 #[rustfmt::skip]
1764 let expected = Buffer::with_lines([
1765 "┌╌╌╌╌╌╌╌╌┐",
1766 "╎ ╎",
1767 "└╌╌╌╌╌╌╌╌┘",
1768 ]);
1769 assert_eq!(buffer, expected);
1770 }
1771
1772 #[test]
1773 fn render_heavy_double_dashed_border() {
1774 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
1775 Block::bordered()
1776 .border_type(BorderType::HeavyDoubleDashed)
1777 .render(buffer.area, &mut buffer);
1778 #[rustfmt::skip]
1779 let expected = Buffer::with_lines([
1780 "┏╍╍╍╍╍╍╍╍┓",
1781 "╏ ╏",
1782 "┗╍╍╍╍╍╍╍╍┛",
1783 ]);
1784 assert_eq!(buffer, expected);
1785 }
1786
1787 #[test]
1788 fn render_light_triple_dashed_border() {
1789 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
1790 Block::bordered()
1791 .border_type(BorderType::LightTripleDashed)
1792 .render(buffer.area, &mut buffer);
1793 #[rustfmt::skip]
1794 let expected = Buffer::with_lines([
1795 "┌┄┄┄┄┄┄┄┄┐",
1796 "┆ ┆",
1797 "└┄┄┄┄┄┄┄┄┘",
1798 ]);
1799 assert_eq!(buffer, expected);
1800 }
1801
1802 #[test]
1803 fn render_heavy_triple_dashed_border() {
1804 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
1805 Block::bordered()
1806 .border_type(BorderType::HeavyTripleDashed)
1807 .render(buffer.area, &mut buffer);
1808 #[rustfmt::skip]
1809 let expected = Buffer::with_lines([
1810 "┏┅┅┅┅┅┅┅┅┓",
1811 "┇ ┇",
1812 "┗┅┅┅┅┅┅┅┅┛",
1813 ]);
1814 assert_eq!(buffer, expected);
1815 }
1816
1817 #[test]
1818 fn render_light_quadruple_dashed_border() {
1819 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
1820 Block::bordered()
1821 .border_type(BorderType::LightQuadrupleDashed)
1822 .render(buffer.area, &mut buffer);
1823 #[rustfmt::skip]
1824 let expected = Buffer::with_lines([
1825 "┌┈┈┈┈┈┈┈┈┐",
1826 "┊ ┊",
1827 "└┈┈┈┈┈┈┈┈┘",
1828 ]);
1829 assert_eq!(buffer, expected);
1830 }
1831
1832 #[test]
1833 fn render_heavy_quadruple_dashed_border() {
1834 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
1835 Block::bordered()
1836 .border_type(BorderType::HeavyQuadrupleDashed)
1837 .render(buffer.area, &mut buffer);
1838 #[rustfmt::skip]
1839 let expected = Buffer::with_lines([
1840 "┏┉┉┉┉┉┉┉┉┓",
1841 "┋ ┋",
1842 "┗┉┉┉┉┉┉┉┉┛",
1843 ]);
1844 assert_eq!(buffer, expected);
1845 }
1846
1847 #[test]
1848 fn render_custom_border_set() {
1849 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
1850 Block::bordered()
1851 .border_set(border::Set {
1852 top_left: "1",
1853 top_right: "2",
1854 bottom_left: "3",
1855 bottom_right: "4",
1856 vertical_left: "L",
1857 vertical_right: "R",
1858 horizontal_top: "T",
1859 horizontal_bottom: "B",
1860 })
1861 .render(buffer.area, &mut buffer);
1862 #[rustfmt::skip]
1863 let expected = Buffer::with_lines([
1864 "1TTTTTTTT2",
1865 "L R",
1866 "3BBBBBBBB4",
1867 ]);
1868 assert_eq!(buffer, expected);
1869 }
1870
1871 #[rstest]
1872 #[case::replace(MergeStrategy::Replace)]
1873 #[case::exact(MergeStrategy::Exact)]
1874 #[case::fuzzy(MergeStrategy::Fuzzy)]
1875 fn render_partial_borders(#[case] strategy: MergeStrategy) {
1876 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
1877 Block::new()
1878 .borders(Borders::TOP | Borders::LEFT | Borders::RIGHT | Borders::BOTTOM)
1879 .merge_borders(strategy)
1880 .render(buffer.area, &mut buffer);
1881 #[rustfmt::skip]
1882 let expected = Buffer::with_lines([
1883 "┌────────┐",
1884 "│ │",
1885 "└────────┘",
1886 ]);
1887 assert_eq!(buffer, expected);
1888
1889 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
1890 Block::new()
1891 .borders(Borders::TOP | Borders::LEFT)
1892 .merge_borders(strategy)
1893 .render(buffer.area, &mut buffer);
1894 #[rustfmt::skip]
1895 let expected = Buffer::with_lines([
1896 "┌─────────",
1897 "│ ",
1898 "│ ",
1899 ]);
1900 assert_eq!(buffer, expected);
1901
1902 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
1903 Block::new()
1904 .borders(Borders::TOP | Borders::RIGHT)
1905 .merge_borders(strategy)
1906 .render(buffer.area, &mut buffer);
1907 #[rustfmt::skip]
1908 let expected = Buffer::with_lines([
1909 "─────────┐",
1910 " │",
1911 " │",
1912 ]);
1913 assert_eq!(buffer, expected);
1914
1915 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
1916 Block::new()
1917 .borders(Borders::BOTTOM | Borders::LEFT)
1918 .merge_borders(strategy)
1919 .render(buffer.area, &mut buffer);
1920 #[rustfmt::skip]
1921 let expected = Buffer::with_lines([
1922 "│ ",
1923 "│ ",
1924 "└─────────",
1925 ]);
1926 assert_eq!(buffer, expected);
1927
1928 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
1929 Block::new()
1930 .borders(Borders::BOTTOM | Borders::RIGHT)
1931 .merge_borders(strategy)
1932 .render(buffer.area, &mut buffer);
1933 #[rustfmt::skip]
1934 let expected = Buffer::with_lines([
1935 " │",
1936 " │",
1937 "─────────┘",
1938 ]);
1939 assert_eq!(buffer, expected);
1940
1941 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
1942 Block::new()
1943 .borders(Borders::TOP | Borders::BOTTOM)
1944 .merge_borders(strategy)
1945 .render(buffer.area, &mut buffer);
1946 #[rustfmt::skip]
1947 let expected = Buffer::with_lines([
1948 "──────────",
1949 " ",
1950 "──────────",
1951 ]);
1952 assert_eq!(buffer, expected);
1953
1954 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
1955 Block::new()
1956 .borders(Borders::LEFT | Borders::RIGHT)
1957 .merge_borders(strategy)
1958 .render(buffer.area, &mut buffer);
1959 #[rustfmt::skip]
1960 let expected = Buffer::with_lines([
1961 "│ │",
1962 "│ │",
1963 "│ │",
1964 ]);
1965 assert_eq!(buffer, expected);
1966 }
1967
1968 /// Renders a series of blocks with all the possible border types and merges them according to
1969 /// the specified strategy. The resulting buffer is compared against the expected output for
1970 /// each merge strategy.
1971 ///
1972 /// At some point, it might be convenient to replace the manual `include_str!` calls with
1973 /// [insta](https://crates.io/crates/insta)
1974 #[rstest]
1975 #[case::replace(MergeStrategy::Replace, include_str!("../tests/block/merge_replace.txt"))]
1976 #[case::exact(MergeStrategy::Exact, include_str!("../tests/block/merge_exact.txt"))]
1977 #[case::fuzzy(MergeStrategy::Fuzzy, include_str!("../tests/block/merge_fuzzy.txt"))]
1978 fn render_merged_borders(#[case] strategy: MergeStrategy, #[case] expected: &'static str) {
1979 let border_types = [
1980 BorderType::Plain,
1981 BorderType::Rounded,
1982 BorderType::Thick,
1983 BorderType::Double,
1984 BorderType::LightDoubleDashed,
1985 BorderType::HeavyDoubleDashed,
1986 BorderType::LightTripleDashed,
1987 BorderType::HeavyTripleDashed,
1988 BorderType::LightQuadrupleDashed,
1989 BorderType::HeavyQuadrupleDashed,
1990 ];
1991 let rects = [
1992 // touching at corners
1993 (Rect::new(0, 0, 5, 5), Rect::new(4, 4, 5, 5)),
1994 // overlapping
1995 (Rect::new(10, 0, 5, 5), Rect::new(12, 2, 5, 5)),
1996 // touching vertical edges
1997 (Rect::new(18, 0, 5, 5), Rect::new(22, 0, 5, 5)),
1998 // touching horizontal edges
1999 (Rect::new(28, 0, 5, 5), Rect::new(28, 4, 5, 5)),
2000 ];
2001
2002 let mut buffer = Buffer::empty(Rect::new(0, 0, 43, 1000));
2003
2004 let mut offset = Offset::ZERO;
2005 for (border_type_1, border_type_2) in iproduct!(border_types, border_types) {
2006 let title = format!("{border_type_1} + {border_type_2}");
2007 let title_area = Rect::new(0, 0, 43, 1) + offset;
2008 title.render(title_area, &mut buffer);
2009 offset.y += 1;
2010 for (rect_1, rect_2) in rects {
2011 Block::bordered()
2012 .border_type(border_type_1)
2013 .merge_borders(strategy)
2014 .render(rect_1 + offset, &mut buffer);
2015 Block::bordered()
2016 .border_type(border_type_2)
2017 .merge_borders(strategy)
2018 .render(rect_2 + offset, &mut buffer);
2019 }
2020 offset.y += 9;
2021 }
2022 pretty_assertions::assert_eq!(Buffer::with_lines(expected.lines()), buffer);
2023 }
2024
2025 #[rstest]
2026 #[case::replace(MergeStrategy::Replace, Buffer::with_lines([
2027 "┏block top━━┓",
2028 "┃ ┃",
2029 "┗━━━━━━━━━━━┛",
2030 "│ │",
2031 "└───────────┘",
2032 ])
2033 )]
2034 #[case::replace(MergeStrategy::Exact, Buffer::with_lines([
2035 "┏block top━━┓",
2036 "┃ ┃",
2037 "┡block btm━━┩",
2038 "│ │",
2039 "└───────────┘",
2040 ])
2041 )]
2042 #[case::replace(MergeStrategy::Fuzzy, Buffer::with_lines([
2043 "┏block top━━┓",
2044 "┃ ┃",
2045 "┡block btm━━┩",
2046 "│ │",
2047 "└───────────┘",
2048 ])
2049 )]
2050 fn merged_titles_bottom_first(#[case] strategy: MergeStrategy, #[case] expected: Buffer) {
2051 let mut buffer = Buffer::empty(Rect::new(0, 0, 13, 5));
2052 Block::bordered()
2053 .title("block btm")
2054 .render(Rect::new(0, 2, 13, 3), &mut buffer);
2055 Block::bordered()
2056 .title("block top")
2057 .border_type(BorderType::Thick)
2058 .merge_borders(strategy)
2059 .render(Rect::new(0, 0, 13, 3), &mut buffer);
2060 assert_eq!(buffer, expected);
2061 }
2062
2063 #[rstest]
2064 #[case::replace(MergeStrategy::Replace, Buffer::with_lines([
2065 "┏block top━━┓",
2066 "┃ ┃",
2067 "┌block btm──┐",
2068 "│ │",
2069 "└───────────┘",
2070 ])
2071 )]
2072 #[case::replace(MergeStrategy::Exact, Buffer::with_lines([
2073 "┏block top━━┓",
2074 "┃ ┃",
2075 "┞block btm──┦",
2076 "│ │",
2077 "└───────────┘",
2078 ])
2079 )]
2080 #[case::replace(MergeStrategy::Fuzzy, Buffer::with_lines([
2081 "┏block top━━┓",
2082 "┃ ┃",
2083 "┞block btm──┦",
2084 "│ │",
2085 "└───────────┘",
2086 ])
2087 )]
2088 fn merged_titles_top_first(#[case] strategy: MergeStrategy, #[case] expected: Buffer) {
2089 let mut buffer = Buffer::empty(Rect::new(0, 0, 13, 5));
2090 Block::bordered()
2091 .title("block top")
2092 .border_type(BorderType::Thick)
2093 .render(Rect::new(0, 0, 13, 3), &mut buffer);
2094 Block::bordered()
2095 .title("block btm")
2096 .merge_borders(strategy)
2097 .render(Rect::new(0, 2, 13, 3), &mut buffer);
2098 assert_eq!(buffer, expected);
2099 }
2100
2101 #[test]
2102 fn left_titles() {
2103 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 1));
2104 Block::new()
2105 .title("L12")
2106 .title("L34")
2107 .render(buffer.area, &mut buffer);
2108 assert_eq!(buffer, Buffer::with_lines(["L12 L34 "]));
2109 }
2110
2111 #[test]
2112 fn left_titles_truncated() {
2113 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 1));
2114 Block::new()
2115 .title("L12345")
2116 .title("L67890")
2117 .render(buffer.area, &mut buffer);
2118 assert_eq!(buffer, Buffer::with_lines(["L12345 L67"]));
2119 }
2120
2121 #[test]
2122 fn center_titles() {
2123 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 1));
2124 Block::new()
2125 .title(Line::from("C12").centered())
2126 .title(Line::from("C34").centered())
2127 .render(buffer.area, &mut buffer);
2128 assert_eq!(buffer, Buffer::with_lines([" C12 C34 "]));
2129 }
2130
2131 #[test]
2132 fn center_titles_truncated() {
2133 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 1));
2134 Block::new()
2135 .title(Line::from("C12345").centered())
2136 .title(Line::from("C67890").centered())
2137 .render(buffer.area, &mut buffer);
2138 assert_eq!(buffer, Buffer::with_lines(["12345 C678"]));
2139 }
2140
2141 #[test]
2142 fn right_titles() {
2143 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 1));
2144 Block::new()
2145 .title(Line::from("R12").right_aligned())
2146 .title(Line::from("R34").right_aligned())
2147 .render(buffer.area, &mut buffer);
2148 assert_eq!(buffer, Buffer::with_lines([" R12 R34"]));
2149 }
2150
2151 #[test]
2152 fn right_titles_truncated() {
2153 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 1));
2154 Block::new()
2155 .title(Line::from("R12345").right_aligned())
2156 .title(Line::from("R67890").right_aligned())
2157 .render(buffer.area, &mut buffer);
2158 assert_eq!(buffer, Buffer::with_lines(["345 R67890"]));
2159 }
2160
2161 #[test]
2162 fn center_title_truncates_left_title() {
2163 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 1));
2164 Block::new()
2165 .title("L1234")
2166 .title(Line::from("C5678").centered())
2167 .render(buffer.area, &mut buffer);
2168 assert_eq!(buffer, Buffer::with_lines(["L1C5678 "]));
2169 }
2170
2171 #[test]
2172 fn right_title_truncates_left_title() {
2173 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 1));
2174 Block::new()
2175 .title("L12345")
2176 .title(Line::from("R67890").right_aligned())
2177 .render(buffer.area, &mut buffer);
2178 assert_eq!(buffer, Buffer::with_lines(["L123R67890"]));
2179 }
2180
2181 #[test]
2182 fn right_title_truncates_center_title() {
2183 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 1));
2184 Block::new()
2185 .title(Line::from("C12345").centered())
2186 .title(Line::from("R67890").right_aligned())
2187 .render(buffer.area, &mut buffer);
2188 assert_eq!(buffer, Buffer::with_lines([" C1R67890"]));
2189 }
2190
2191 #[test]
2192 fn render_in_minimal_buffer() {
2193 let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
2194 // This should not panic, even if the buffer is too small to render the block.
2195 Block::bordered()
2196 .title("I'm too big for this buffer")
2197 .padding(Padding::uniform(10))
2198 .render(buffer.area, &mut buffer);
2199 assert_eq!(buffer, Buffer::with_lines(["┌"]));
2200 }
2201
2202 #[test]
2203 fn render_in_zero_size_buffer() {
2204 let mut buffer = Buffer::empty(Rect::ZERO);
2205 // This should not panic, even if the buffer has zero size.
2206 Block::bordered()
2207 .title("I'm too big for this buffer")
2208 .padding(Padding::uniform(10))
2209 .render(buffer.area, &mut buffer);
2210 }
2211
2212 /// Regression tests for previously panic-prone arithmetic paths.
2213 ///
2214 /// These cases intentionally exercise pathological geometry and very large title widths to
2215 /// ensure the helper methods stay panic-free and produce bounded results.
2216 ///
2217 /// This module is gated on `debug_assertions` because Rust wraps primitive integer arithmetic
2218 /// in release builds, while debug builds panic on unchecked overflow and underflow. The cases
2219 /// here were derived from debug-mode failures and are kept to prevent those edge paths from
2220 /// becoming panic-prone again.
2221 #[cfg(debug_assertions)]
2222 mod regression {
2223 use super::*;
2224
2225 /// Summing large padding values must not overflow before the saturating subtraction.
2226 #[rstest]
2227 #[case(Padding::new(u16::MAX, 1, 0, 0), Rect::new(u16::MAX, 0, 0, 1))]
2228 #[case(Padding::new(0, 0, u16::MAX, 1), Rect::new(0, u16::MAX, 1, 0))]
2229 fn inner_saturates_when_padding_sum_overflows(
2230 #[case] padding: Padding,
2231 #[case] expected: Rect,
2232 ) {
2233 let block = Block::new().padding(padding);
2234 assert_eq!(block.inner(Rect::new(0, 0, 1, 1)), expected);
2235 }
2236
2237 /// Empty areas must not underflow when the side renderer subtracts one from the edges.
2238 #[test]
2239 fn render_sides_handles_empty_area_without_panicking() {
2240 let block = Block::bordered();
2241 let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
2242 block.render_sides(Rect::ZERO, &mut buffer);
2243 assert_eq!(buffer, Buffer::with_lines(["─"]));
2244 }
2245
2246 /// Empty areas must not underflow when corner coordinates use the right or bottom edge.
2247 #[test]
2248 fn render_corners_handles_empty_area_without_panicking() {
2249 let block = Block::bordered();
2250 let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
2251 block.render_corners(Rect::ZERO, &mut buffer);
2252 assert_eq!(buffer, Buffer::with_lines(["┌"]));
2253 }
2254
2255 /// Tiny merged-border areas must remain bounded instead of indexing past the buffer.
2256 #[rstest]
2257 #[case::exact(MergeStrategy::Exact)]
2258 #[case::fuzzy(MergeStrategy::Fuzzy)]
2259 fn render_merged_borders_in_minimal_buffer_does_not_panic(#[case] strategy: MergeStrategy) {
2260 let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
2261 Block::bordered()
2262 .merge_borders(strategy)
2263 .render(buffer.area, &mut buffer);
2264 assert_eq!(buffer, Buffer::with_lines(["┼"]));
2265 }
2266
2267 /// A single huge title must not overflow when accounting for the trailing spacer.
2268 #[test]
2269 fn render_center_titles_handles_title_width_increment_overflow() {
2270 let block = Block::new().title(Line::from("a".repeat(u16::MAX as usize)).centered());
2271 let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
2272 block.render_center_titles(TitlePosition::Top, Rect::new(0, 0, 1, 1), &mut buffer);
2273 assert_eq!(buffer, Buffer::with_lines([" "]));
2274 }
2275
2276 /// Accumulating centered-title widths must not overflow the running total.
2277 #[test]
2278 fn render_center_titles_handles_total_width_overflow() {
2279 let block = Block::new()
2280 .title(Line::from("a".repeat(40_000)).centered())
2281 .title(Line::from("b".repeat(30_000)).centered());
2282 let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
2283 block.render_center_titles(TitlePosition::Top, Rect::new(0, 0, 1, 1), &mut buffer);
2284 assert_eq!(buffer, Buffer::with_lines([" "]));
2285 }
2286
2287 /// Centering logic must stay bounded when the input area sits at the maximum x offset.
2288 #[test]
2289 fn render_centered_titles_without_truncation_handles_maximum_x() {
2290 let block = Block::new();
2291 let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
2292 block.render_centered_titles_without_truncation(
2293 Vec::new(),
2294 0,
2295 Rect::new(u16::MAX - 1, 0, 1, 1),
2296 &mut buffer,
2297 );
2298 assert_eq!(buffer, Buffer::with_lines([" "]));
2299 }
2300
2301 /// Advancing after a very wide centered title must not overflow `width + 1`.
2302 #[test]
2303 fn render_centered_titles_without_truncation_handles_title_advance_overflow() {
2304 let block = Block::new();
2305 let title = Line::from("a".repeat(u16::MAX as usize)).centered();
2306 let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
2307 block.render_centered_titles_without_truncation(
2308 vec![&title],
2309 u16::MAX,
2310 Rect::new(0, 0, 1, 1),
2311 &mut buffer,
2312 );
2313 assert_eq!(buffer, Buffer::with_lines(["a"]));
2314 }
2315
2316 /// The truncating centered-title path must also bound `width + 1` when advancing.
2317 #[test]
2318 fn render_centered_titles_with_truncation_handles_title_advance_overflow() {
2319 let block = Block::new();
2320 let title = Line::from("a".repeat(u16::MAX as usize)).centered();
2321 let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
2322 block.render_centered_titles_with_truncation(
2323 vec![&title],
2324 u16::MAX,
2325 Rect::new(0, 0, u16::MAX, 1),
2326 &mut buffer,
2327 );
2328 assert_eq!(buffer, Buffer::with_lines(["a"]));
2329 }
2330
2331 /// Left-title rendering must bound `title_width + 1` when moving to the next title.
2332 #[test]
2333 fn render_left_titles_handles_title_advance_overflow() {
2334 let block = Block::new().title("a".repeat(u16::MAX as usize));
2335 let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
2336 block.render_left_titles(TitlePosition::Top, Rect::new(0, 0, 1, 1), &mut buffer);
2337 assert_eq!(buffer, Buffer::with_lines(["a"]));
2338 }
2339
2340 /// Offsetting the title area by the left border must stay bounded at the `u16` edge.
2341 #[test]
2342 fn titles_area_saturates_when_left_border_offset_overflows() {
2343 let block = Block::new().borders(Borders::LEFT);
2344 assert_eq!(
2345 block.titles_area(Rect::new(u16::MAX, 0, 1, 1), TitlePosition::Top),
2346 Rect::new(u16::MAX, 0, 1, 1)
2347 );
2348 }
2349
2350 /// Bottom title positioning must not underflow when the area height is zero.
2351 #[test]
2352 fn titles_area_handles_empty_area_without_panicking() {
2353 let block = Block::new();
2354 assert_eq!(
2355 block.titles_area(Rect::ZERO, TitlePosition::Bottom),
2356 Rect::new(0, 0, 0, 1)
2357 );
2358 }
2359
2360 /// Adding title or border space to maximal padding must not overflow.
2361 #[rstest]
2362 #[case(Block::new().padding(Padding::new(0, 0, u16::MAX, 0)).title_top("T"), (u16::MAX, 0))]
2363 #[case(Block::new().padding(Padding::new(0, 0, 0, u16::MAX)).title_bottom("T"), (0, u16::MAX))]
2364 fn vertical_space_saturates_when_space_overflows(
2365 #[case] block: Block,
2366 #[case] expected: (u16, u16),
2367 ) {
2368 assert_eq!(block.vertical_space(), expected);
2369 }
2370 }
2371}