Skip to main content

urushi/style/
block.rs

1//! The [`BlockStyle`] builder: a rectangle, and the style filling the geometry
2//! it creates.
3
4use crate::view::Size;
5use crate::{
6    Align, Border, Color, Hyperlink, Length, Overflow, Sides, TextAttribute, TextAttributes,
7    TextStyle, Underline, UnderlineStyle, VerticalAlign,
8};
9
10/// A rectangle: padding, margin, border, dimensions, alignment, and the
11/// [`TextStyle`] that fills the geometry they create.
12///
13/// A `BlockStyle` is an immutable value, like [`TextStyle`]. Its text properties —
14/// colors and attributes — are the style of the block's own fill: padding rows,
15/// alignment gaps, and text explicitly built from [`BlockStyle::text_style`]. A
16/// block's style does not flow into a child view; each child carries its own
17/// complete value.
18///
19/// ```
20/// use urushi::{BlockStyle, Border, TextStyle, View};
21///
22/// let panel = BlockStyle::new().border(Border::ROUNDED).padding((0, 1));
23/// let view = View::block(panel, View::text("こんにちは, urushi!", TextStyle::new()));
24/// ```
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct BlockStyle {
27    text_style: TextStyle,
28    padding: Sides,
29    margin: Sides,
30    border: Option<Border>,
31    border_top: bool,
32    border_right: bool,
33    border_bottom: bool,
34    border_left: bool,
35    border_text_style: TextStyle,
36    border_foreground: Option<Color>,
37    border_background: Option<Color>,
38    width: Option<Length>,
39    height: Option<Length>,
40    min_width: Option<u16>,
41    min_height: Option<u16>,
42    max_width: Option<u16>,
43    max_height: Option<u16>,
44    overflow: Overflow,
45    align: Align,
46    vertical_align: VerticalAlign,
47}
48
49impl Default for BlockStyle {
50    fn default() -> Self {
51        Self {
52            text_style: TextStyle::new(),
53            padding: Sides::default(),
54            margin: Sides::default(),
55            border: None,
56            border_top: true,
57            border_right: true,
58            border_bottom: true,
59            border_left: true,
60            border_text_style: TextStyle::new(),
61            border_foreground: None,
62            border_background: None,
63            width: None,
64            height: None,
65            min_width: None,
66            min_height: None,
67            max_width: None,
68            max_height: None,
69            overflow: Overflow::default(),
70            align: Align::default(),
71            vertical_align: VerticalAlign::default(),
72        }
73    }
74}
75
76impl BlockStyle {
77    pub fn new() -> Self {
78        Self::default()
79    }
80
81    /// Creates a block style filled with `text`.
82    pub fn from_text_style(text_style: TextStyle) -> Self {
83        Self {
84            text_style,
85            ..Self::default()
86        }
87    }
88
89    /// Returns the style filling this block.
90    pub const fn text_style(&self) -> &TextStyle {
91        &self.text_style
92    }
93
94    /// Sets the fill (and text) foreground color.
95    pub fn foreground(mut self, color: impl Into<Color>) -> Self {
96        self.text_style = self.text_style.foreground(color);
97        self
98    }
99
100    /// Returns the fill foreground color to the terminal default.
101    pub fn reset_foreground(mut self) -> Self {
102        self.text_style = self.text_style.reset_foreground();
103        self
104    }
105
106    /// Sets the fill (and text) background color.
107    pub fn background(mut self, color: impl Into<Color>) -> Self {
108        self.text_style = self.text_style.background(color);
109        self
110    }
111
112    /// Returns the fill background color to the terminal default.
113    pub fn reset_background(mut self) -> Self {
114        self.text_style = self.text_style.reset_background();
115        self
116    }
117
118    /// Adds one active fill text attribute.
119    pub fn add_attribute(mut self, attribute: TextAttribute) -> Self {
120        self.text_style = self.text_style.add_attribute(attribute);
121        self
122    }
123
124    /// Adds a set of active fill text attributes.
125    pub fn add_attributes(mut self, attributes: TextAttributes) -> Self {
126        self.text_style = self.text_style.add_attributes(attributes);
127        self
128    }
129
130    /// Removes one active fill text attribute.
131    pub fn remove_attribute(mut self, attribute: TextAttribute) -> Self {
132        self.text_style = self.text_style.remove_attribute(attribute);
133        self
134    }
135
136    /// Removes a set of active fill text attributes.
137    pub fn remove_attributes(mut self, attributes: TextAttributes) -> Self {
138        self.text_style = self.text_style.remove_attributes(attributes);
139        self
140    }
141
142    /// Restores the active fill text attributes to their default value.
143    pub fn reset_attributes(mut self) -> Self {
144        self.text_style = self.text_style.reset_attributes();
145        self
146    }
147
148    pub fn bold(self) -> Self {
149        self.add_attribute(TextAttribute::Bold)
150    }
151
152    pub fn dim(self) -> Self {
153        self.add_attribute(TextAttribute::Dim)
154    }
155
156    pub fn italic(self) -> Self {
157        self.add_attribute(TextAttribute::Italic)
158    }
159
160    /// Underlines the fill text with a single line in the foreground color.
161    pub fn underlined(mut self) -> Self {
162        self.text_style = self.text_style.underlined();
163        self
164    }
165
166    /// Sets the shape the fill text's underline is drawn with, adding an
167    /// underline in the foreground color when the style has none.
168    pub fn underline_style(mut self, style: UnderlineStyle) -> Self {
169        self.text_style = self.text_style.underline_style(style);
170        self
171    }
172
173    /// Sets the color the fill text's underline is drawn in, adding a single
174    /// underline when the style has none.
175    pub fn underline_color(mut self, color: impl Into<Color>) -> Self {
176        self.text_style = self.text_style.underline_color(color);
177        self
178    }
179
180    /// Replaces the complete fill text underline value.
181    pub fn underline(mut self, underline: Underline) -> Self {
182        self.text_style = self.text_style.underline(underline);
183        self
184    }
185
186    /// Removes the fill text underline, including its color.
187    pub fn reset_underline(mut self) -> Self {
188        self.text_style = self.text_style.reset_underline();
189        self
190    }
191
192    /// Attaches an OSC 8 hyperlink to the fill text.
193    pub fn hyperlink(mut self, hyperlink: impl Into<Hyperlink>) -> Self {
194        self.text_style = self.text_style.hyperlink(hyperlink);
195        self
196    }
197
198    /// Removes the OSC 8 hyperlink from the fill text.
199    pub fn reset_hyperlink(mut self) -> Self {
200        self.text_style = self.text_style.reset_hyperlink();
201        self
202    }
203
204    pub fn blink(self) -> Self {
205        self.add_attribute(TextAttribute::SlowBlink)
206    }
207
208    pub fn reverse(self) -> Self {
209        self.add_attribute(TextAttribute::Reversed)
210    }
211
212    pub fn hide(self) -> Self {
213        self.add_attribute(TextAttribute::Hidden)
214    }
215
216    pub fn strikethrough(self) -> Self {
217        self.add_attribute(TextAttribute::CrossedOut)
218    }
219
220    /// Sets padding between the content and the border.
221    pub fn padding(mut self, sides: impl Into<Sides>) -> Self {
222        self.padding = sides.into();
223        self
224    }
225
226    /// Restores the block padding to its default value.
227    pub fn reset_padding(mut self) -> Self {
228        self.padding = Sides::default();
229        self
230    }
231
232    /// Sets unstyled spacing outside the border.
233    pub fn margin(mut self, sides: impl Into<Sides>) -> Self {
234        self.margin = sides.into();
235        self
236    }
237
238    /// Restores the block margin to its default value.
239    pub fn reset_margin(mut self) -> Self {
240        self.margin = Sides::default();
241        self
242    }
243
244    /// Sets the border glyphs around the padded content.
245    ///
246    /// A new style enables all four edges. Use the `border_*` builders to
247    /// configure edge visibility independently.
248    pub fn border(mut self, border: Border) -> Self {
249        self.border = Some(border);
250        self
251    }
252
253    /// Removes the border glyph set. Edge configuration is retained.
254    pub fn reset_border(mut self) -> Self {
255        self.border = None;
256        self
257    }
258
259    /// Enables or disables the top border edge.
260    pub fn border_top(mut self, enabled: bool) -> Self {
261        self.border_top = enabled;
262        self
263    }
264
265    /// Restores top-edge visibility to its default value.
266    pub fn reset_border_top(mut self) -> Self {
267        self.border_top = true;
268        self
269    }
270
271    /// Enables or disables the right border edge.
272    pub fn border_right(mut self, enabled: bool) -> Self {
273        self.border_right = enabled;
274        self
275    }
276
277    /// Restores right-edge visibility to its default value.
278    pub fn reset_border_right(mut self) -> Self {
279        self.border_right = true;
280        self
281    }
282
283    /// Enables or disables the bottom border edge.
284    pub fn border_bottom(mut self, enabled: bool) -> Self {
285        self.border_bottom = enabled;
286        self
287    }
288
289    /// Restores bottom-edge visibility to its default value.
290    pub fn reset_border_bottom(mut self) -> Self {
291        self.border_bottom = true;
292        self
293    }
294
295    /// Enables or disables the left border edge.
296    pub fn border_left(mut self, enabled: bool) -> Self {
297        self.border_left = enabled;
298        self
299    }
300
301    /// Restores left-edge visibility to its default value.
302    pub fn reset_border_left(mut self) -> Self {
303        self.border_left = true;
304        self
305    }
306
307    /// Replaces the complete logical style used for border glyphs.
308    ///
309    /// A subsequently applied border foreground or background overrides the
310    /// corresponding property in this style.
311    pub fn border_text_style(mut self, style: TextStyle) -> Self {
312        self.border_text_style = style;
313        self
314    }
315
316    /// Restores the complete logical border-glyph style to its default value.
317    pub fn reset_border_text_style(mut self) -> Self {
318        self.border_text_style = TextStyle::default();
319        self
320    }
321
322    /// Sets the border foreground color.
323    pub fn border_foreground(mut self, color: impl Into<Color>) -> Self {
324        self.border_foreground = Some(color.into());
325        self
326    }
327
328    /// Removes the border foreground override.
329    pub fn reset_border_foreground(mut self) -> Self {
330        self.border_foreground = None;
331        self
332    }
333
334    /// Sets the border background color.
335    pub fn border_background(mut self, color: impl Into<Color>) -> Self {
336        self.border_background = Some(color.into());
337        self
338    }
339
340    /// Removes the border background override.
341    pub fn reset_border_background(mut self) -> Self {
342        self.border_background = None;
343        self
344    }
345
346    /// Sets the width of the box: content plus padding plus enabled border
347    /// edges, with margin outside it.
348    ///
349    /// The absence of a width means auto — the content's own width. Content
350    /// wider than the resolved box is absorbed by [`BlockStyle::overflow`];
351    /// the frame closes at the resolved width either way.
352    pub fn width(mut self, width: impl Into<Length>) -> Self {
353        self.width = Some(width.into());
354        self
355    }
356
357    /// Returns the width to auto sizing.
358    pub fn reset_width(mut self) -> Self {
359        self.width = None;
360        self
361    }
362
363    /// Sets the height of the box: content plus padding plus enabled border
364    /// edges, with margin outside it.
365    ///
366    /// This is a size, not a minimum: taller content is clipped inside the
367    /// frame rather than growing the box. The absence of a height means auto.
368    pub fn height(mut self, height: impl Into<Length>) -> Self {
369        self.height = Some(height.into());
370        self
371    }
372
373    /// Returns the height to auto sizing.
374    pub fn reset_height(mut self) -> Self {
375        self.height = None;
376        self
377    }
378
379    /// Sets the width below which the box does not shrink.
380    pub fn min_width(mut self, width: u16) -> Self {
381        self.min_width = Some(width);
382        self
383    }
384
385    /// Removes the minimum width.
386    pub fn reset_min_width(mut self) -> Self {
387        self.min_width = None;
388        self
389    }
390
391    /// Sets the height below which the box does not shrink.
392    pub fn min_height(mut self, height: u16) -> Self {
393        self.min_height = Some(height);
394        self
395    }
396
397    /// Removes the minimum height.
398    pub fn reset_min_height(mut self) -> Self {
399        self.min_height = None;
400        self
401    }
402
403    /// Bounds the box's width. The box shrinks to fit its content and never
404    /// exceeds this bound; the bound never cuts the frame.
405    pub fn max_width(mut self, width: u16) -> Self {
406        self.max_width = Some(width);
407        self
408    }
409
410    /// Removes the maximum width.
411    pub fn reset_max_width(mut self) -> Self {
412        self.max_width = None;
413        self
414    }
415
416    /// Bounds the box's height. The box shrinks to fit its content and never
417    /// exceeds this bound; the bound never cuts the frame.
418    pub fn max_height(mut self, height: u16) -> Self {
419        self.max_height = Some(height);
420        self
421    }
422
423    /// Removes the maximum height.
424    pub fn reset_max_height(mut self) -> Self {
425        self.max_height = None;
426        self
427    }
428
429    /// Sets how content wider than the box is absorbed.
430    pub fn overflow(mut self, overflow: Overflow) -> Self {
431        self.overflow = overflow;
432        self
433    }
434
435    /// Restores the overflow policy to its default value.
436    pub fn reset_overflow(mut self) -> Self {
437        self.overflow = Overflow::default();
438        self
439    }
440
441    /// Sets the horizontal alignment of content within the box.
442    pub fn align(mut self, align: Align) -> Self {
443        self.align = align;
444        self
445    }
446
447    /// Restores horizontal alignment to its default value.
448    pub fn reset_align(mut self) -> Self {
449        self.align = Align::default();
450        self
451    }
452
453    /// Sets the vertical alignment of content within a fixed-height box.
454    pub fn vertical_align(mut self, align: VerticalAlign) -> Self {
455        self.vertical_align = align;
456        self
457    }
458
459    /// Restores vertical alignment to its default value.
460    pub fn reset_vertical_align(mut self) -> Self {
461        self.vertical_align = VerticalAlign::default();
462        self
463    }
464
465    /// Returns the fill foreground color instruction, if one is set.
466    pub const fn get_foreground(&self) -> Option<Color> {
467        self.text_style.get_foreground()
468    }
469
470    /// Returns the fill background color instruction, if one is set.
471    pub const fn get_background(&self) -> Option<Color> {
472        self.text_style.get_background()
473    }
474
475    /// Returns the fill text's underline instruction, if one is set.
476    pub const fn get_underline(&self) -> Option<Underline> {
477        self.text_style.get_underline()
478    }
479
480    /// Returns the active fill text attributes.
481    pub const fn get_attributes(&self) -> TextAttributes {
482        self.text_style.get_attributes()
483    }
484
485    /// Returns the padding applied inside the border.
486    pub const fn get_padding(&self) -> Sides {
487        self.padding
488    }
489
490    /// Returns the unstyled margin applied outside the border.
491    pub const fn get_margin(&self) -> Sides {
492        self.margin
493    }
494
495    /// Returns the border glyph set, if a border is enabled.
496    pub const fn get_border(&self) -> Option<Border> {
497        self.border
498    }
499
500    /// Returns whether the top border edge is enabled.
501    pub const fn get_border_top(&self) -> bool {
502        self.border_top
503    }
504
505    /// Returns whether the right border edge is enabled.
506    pub const fn get_border_right(&self) -> bool {
507        self.border_right
508    }
509
510    /// Returns whether the bottom border edge is enabled.
511    pub const fn get_border_bottom(&self) -> bool {
512        self.border_bottom
513    }
514
515    /// Returns whether the left border edge is enabled.
516    pub const fn get_border_left(&self) -> bool {
517        self.border_left
518    }
519
520    /// Returns the complete logical style used as the border-style base.
521    pub const fn get_border_text_style(&self) -> &TextStyle {
522        &self.border_text_style
523    }
524
525    /// Returns the border foreground color instruction.
526    pub const fn get_border_foreground(&self) -> Option<Color> {
527        self.border_foreground
528    }
529
530    /// Returns the border background color instruction.
531    pub const fn get_border_background(&self) -> Option<Color> {
532        self.border_background
533    }
534
535    /// Returns the box's width, if one is set.
536    pub const fn get_width(&self) -> Option<Length> {
537        self.width
538    }
539
540    /// Returns the box's height, if one is set.
541    pub const fn get_height(&self) -> Option<Length> {
542        self.height
543    }
544
545    /// Returns the box's minimum width, if one is set.
546    pub const fn get_min_width(&self) -> Option<u16> {
547        self.min_width
548    }
549
550    /// Returns the box's minimum height, if one is set.
551    pub const fn get_min_height(&self) -> Option<u16> {
552        self.min_height
553    }
554
555    /// Returns the box's maximum width, if one is set.
556    pub const fn get_max_width(&self) -> Option<u16> {
557        self.max_width
558    }
559
560    /// Returns the box's maximum height, if one is set.
561    pub const fn get_max_height(&self) -> Option<u16> {
562        self.max_height
563    }
564
565    /// Returns how content wider than the box is absorbed.
566    pub const fn get_overflow(&self) -> &Overflow {
567        &self.overflow
568    }
569
570    /// Returns the per-axis overhead of enabled border edges plus padding.
571    ///
572    /// This is the conversion between the box a dimension measures and the
573    /// content area inside it: outer minus `frame_size()` is the content area.
574    /// Margin lies outside the box and keeps [`BlockStyle::get_margin`].
575    pub fn frame_size(&self) -> Size {
576        let padding = self.padding;
577        let (left, right, top, bottom) = match self.border {
578            Some(_) => (
579                usize::from(self.border_left),
580                usize::from(self.border_right),
581                usize::from(self.border_top),
582                usize::from(self.border_bottom),
583            ),
584            None => (0, 0, 0, 0),
585        };
586        Size::new(
587            left + right + usize::from(padding.left) + usize::from(padding.right),
588            top + bottom + usize::from(padding.top) + usize::from(padding.bottom),
589        )
590    }
591
592    /// Returns the horizontal alignment within the content box.
593    pub const fn get_align(&self) -> Align {
594        self.align
595    }
596
597    /// Returns the vertical alignment within the content box.
598    pub const fn get_vertical_align(&self) -> VerticalAlign {
599        self.vertical_align
600    }
601
602    /// The style drawn on this block's border glyphs.
603    pub(crate) fn border_style(&self) -> TextStyle {
604        let mut style = self.border_text_style.clone();
605        if let Some(color) = self.border_foreground {
606            style = style.foreground(color);
607        }
608        if let Some(color) = self.border_background {
609            style = style.background(color);
610        }
611        style
612    }
613}
614
615#[cfg(test)]
616mod tests {
617    use super::*;
618
619    #[test]
620    fn reset_builders_restore_every_property_default() {
621        let style = BlockStyle::new()
622            .foreground(Color::RED)
623            .background(Color::BLUE)
624            .add_attribute(TextAttribute::Italic)
625            .underline_color(Color::GREEN)
626            .hyperlink("https://example.com")
627            .padding(1)
628            .margin(2)
629            .border(Border::ROUNDED)
630            .border_top(false)
631            .border_right(false)
632            .border_bottom(false)
633            .border_left(false)
634            .border_text_style(TextStyle::new().bold())
635            .border_foreground(Color::CYAN)
636            .border_background(Color::BLACK)
637            .width(20)
638            .height(4)
639            .min_width(8)
640            .min_height(2)
641            .max_width(30)
642            .max_height(6)
643            .overflow(Overflow::ellipsis())
644            .align(Align::Center)
645            .vertical_align(VerticalAlign::Bottom)
646            .reset_foreground()
647            .reset_background()
648            .reset_attributes()
649            .reset_underline()
650            .reset_hyperlink()
651            .reset_padding()
652            .reset_margin()
653            .reset_border()
654            .reset_border_top()
655            .reset_border_right()
656            .reset_border_bottom()
657            .reset_border_left()
658            .reset_border_text_style()
659            .reset_border_foreground()
660            .reset_border_background()
661            .reset_width()
662            .reset_height()
663            .reset_min_width()
664            .reset_min_height()
665            .reset_max_width()
666            .reset_max_height()
667            .reset_overflow()
668            .reset_align()
669            .reset_vertical_align();
670
671        assert_eq!(style, BlockStyle::new());
672    }
673
674    #[test]
675    fn attribute_operations_accept_sets() {
676        let attributes = TextAttribute::Bold | TextAttribute::Italic;
677        let style = BlockStyle::new()
678            .add_attributes(attributes)
679            .remove_attribute(TextAttribute::Italic);
680
681        assert_eq!(style.get_attributes(), TextAttribute::Bold.into());
682    }
683}