Skip to content

Commit

Permalink
feat(widgets): implement Widget for Widget refs
Browse files Browse the repository at this point in the history
Many widgets can be rendered without changing their state.

This commit implements The `Widget` trait for various references to
widgets and changes their implementations to be immutable.

This allows us to render widgets without consuming them by passing a ref
to the widget when calling `Frame::render_widget()`.

```rust
// this might be stored in a struct
let paragraph = Paragraph::new("Hello world!");

let [left, right] = area.split(&Layout::horizontal([20, 20]));
frame.render_widget(&paragraph, left);
frame.render_widget(&paragraph, right); // we can reuse the widget
```

- Clear
- Block
- Tabs
- Sparkline
- Paragraph
- Gauge
- Calendar

Other widgets will be implemented in follow up commits.

Fixes: #164
Replaces PRs: #122 and
  #16
Enables: #132
Validated as a viable working solution by: #836
  • Loading branch information
joshka committed Jan 20, 2024
1 parent 330a899 commit 45146ce
Show file tree
Hide file tree
Showing 10 changed files with 165 additions and 89 deletions.
5 changes: 1 addition & 4 deletions src/terminal/frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,7 @@ impl Frame<'_> {
/// ```
///
/// [`Layout`]: crate::layout::Layout
pub fn render_widget<W>(&mut self, widget: W, area: Rect)
where
W: Widget,
{
pub fn render_widget<W: Widget>(&mut self, widget: W, area: Rect) {
widget.render(area, self.buffer);
}

Expand Down
19 changes: 18 additions & 1 deletion src/widgets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,24 @@ pub use self::{
};
use crate::{buffer::Buffer, layout::Rect};

/// Base requirements for a Widget
/// A `Widget` is a type that can be drawn on a [`Buffer`] in a given [`Rect`].
///
/// Widgets are created for each frame as they are consumed after rendered. They are not meant to be
/// stored but used as *commands* to draw common figures in the UI.
///
/// ## Examples
///
/// ```rust,no_run
/// use ratatui::{backend::TestBackend, prelude::*, widgets::*};
///
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
///
/// terminal.draw(|frame| {
/// // A widget can be rendered by simply calling its `render` method.
/// frame.render_widget(Clear, frame.size());
/// });
/// ```
pub trait Widget {
/// Draws the current state of the widget in the given buffer. That is the only method required
/// to implement a custom widget.
Expand Down
41 changes: 35 additions & 6 deletions src/widgets/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,25 @@ impl<'a> Block<'a> {
self.padding = padding;
self
}
}

impl Widget for Block<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
(&self).render(area, buf);
}
}

impl Widget for &Block<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
if area.is_empty() {
return;
}
self.render_borders(area, buf);
self.render_titles(area, buf);
}
}

impl Block<'_> {
fn render_borders(&self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style);
let symbols = self.border_set;
Expand Down Expand Up @@ -703,13 +721,24 @@ impl<'a> Block<'a> {
}
}

impl<'a> Widget for Block<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
if area.area() == 0 {
return;
/// An extension trait for [`Block`] that provides some convenience methods.
///
/// This is pub(crate) for now because it is not clear if this is the best way to do this.
pub(crate) trait BlockExt {
/// Render the block if it is `Some` and update the area to the inner area of the block.
/// Otherwise, do nothing.
///
/// This is useful for widgets that have a block as a field and want to render it if it is
/// `Some` and update the area to the inner area of the block.
fn render(&self, area: &mut Rect, buf: &mut Buffer);
}

impl BlockExt for Option<Block<'_>> {
fn render(&self, area: &mut Rect, buf: &mut Buffer) {
if let Some(block) = self {
block.render(*area, buf);
*area = block.inner(*area);
}
self.render_borders(area, buf);
self.render_titles(area, buf);
}
}

Expand Down
53 changes: 30 additions & 23 deletions src/widgets/calendar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use std::collections::HashMap;

use time::{Date, Duration, OffsetDateTime};

use super::block::BlockExt;
use crate::{
prelude::*,
widgets::{Block, Widget},
Expand Down Expand Up @@ -117,43 +118,49 @@ impl<'a, DS: DateStyler> Monthly<'a, DS> {
}
}

impl<'a, DS: DateStyler> Widget for Monthly<'a, DS> {
fn render(mut self, area: Rect, buf: &mut Buffer) {
// Block is used for borders and such
// Draw that first, and use the blank area inside the block for our own purposes
let mut area = match self.block.take() {
None => area,
Some(b) => {
let inner = b.inner(area);
b.render(area, buf);
inner
}
};
impl<DS: DateStyler> Widget for Monthly<'_, DS> {
fn render(self, area: Rect, buf: &mut Buffer) {
(&self).render(area, buf);
}
}

impl<DS: DateStyler> Widget for &Monthly<'_, DS> {
fn render(self, mut area: Rect, buf: &mut Buffer) {
self.block.render(&mut area, buf);
self.render_monthly(area, buf);
}
}

impl<DS: DateStyler> Monthly<'_, DS> {
fn render_monthly(&self, area: Rect, buf: &mut Buffer) {
let layout = Layout::vertical([
Constraint::Length(self.show_month.is_some().into()),
Constraint::Length(self.show_weekday.is_some().into()),
Constraint::Proportional(1),
]);
let [month_header, days_header, days_area] = area.split(&layout);

// Draw the month name and year
if let Some(style) = self.show_month {
let line = Span::styled(
Line::styled(
format!("{} {}", self.display_date.month(), self.display_date.year()),
style,
);
// cal is 21 cells wide, so hard code the 11
let x_off = 11_u16.saturating_sub(line.width() as u16 / 2);
buf.set_line(area.x + x_off, area.y, &line.into(), area.width);
area.y += 1
)
.alignment(Alignment::Center)
.render(month_header, buf);
}

// Draw days of week
if let Some(style) = self.show_weekday {
let days = String::from(" Su Mo Tu We Th Fr Sa");
buf.set_string(area.x, area.y, days, style);
area.y += 1;
Span::styled(" Su Mo Tu We Th Fr Sa", style).render(days_header, buf);
}

// Set the start of the calendar to the Sunday before the 1st (or the sunday of the first)
let first_of_month = self.display_date.replace_day(1).unwrap();
let offset = Duration::days(first_of_month.weekday().number_days_from_sunday().into());
let mut curr_day = first_of_month - offset;

let mut y = days_area.y;
// go through all the weeks containing a day in the target month.
while curr_day.month() as u8 != self.display_date.month().next() as u8 {
let mut spans = Vec::with_capacity(14);
Expand All @@ -168,8 +175,8 @@ impl<'a, DS: DateStyler> Widget for Monthly<'a, DS> {
spans.push(self.format_date(curr_day));
curr_day += Duration::DAY;
}
buf.set_line(area.x, area.y, &spans.into(), area.width);
area.y += 1;
buf.set_line(days_area.x, y, &spans.into(), area.width);
y += 1;
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/widgets/clear.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ use crate::{buffer::Buffer, layout::Rect, widgets::Widget};
pub struct Clear;

impl Widget for Clear {
fn render(self, area: Rect, buf: &mut Buffer) {
(&self).render(area, buf);
}
}

impl Widget for &Clear {
fn render(self, area: Rect, buf: &mut Buffer) {
for x in area.left()..area.right() {
for y in area.top()..area.bottom() {
Expand Down
39 changes: 22 additions & 17 deletions src/widgets/gauge.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#![deny(missing_docs)]
use super::block::BlockExt;
use crate::{
buffer::Buffer,
layout::Rect,
Expand Down Expand Up @@ -161,28 +162,32 @@ impl<'a> Gauge<'a> {
}
}

impl<'a> Widget for Gauge<'a> {
fn render(mut self, area: Rect, buf: &mut Buffer) {
impl Widget for Gauge<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
(&self).render(area, buf);
}
}

impl Widget for &Gauge<'_> {
fn render(self, mut area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style);
let gauge_area = match self.block.take() {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
inner_area
}
None => area,
};
buf.set_style(gauge_area, self.gauge_style);
if gauge_area.height < 1 {
self.block.render(&mut area, buf);
self.render_gague(area, buf);
}
}

impl Gauge<'_> {
fn render_gague(&self, gauge_area: Rect, buf: &mut Buffer) {
if gauge_area.is_empty() {
return;
}

buf.set_style(gauge_area, self.gauge_style);

// compute label value and its position
// label is put at the center of the gauge_area
let label = {
let pct = f64::round(self.ratio * 100.0);
self.label.unwrap_or_else(|| Span::from(format!("{pct}%")))
};
let default_label = Span::raw(format!("{}%", f64::round(self.ratio * 100.0)));
let label = self.label.as_ref().unwrap_or(&default_label);
let clamped_label_width = gauge_area.width.min(label.width() as u16);
let label_col = gauge_area.left() + (gauge_area.width - clamped_label_width) / 2;
let label_row = gauge_area.top() + gauge_area.height / 2;
Expand Down Expand Up @@ -217,7 +222,7 @@ impl<'a> Widget for Gauge<'a> {
}
}
// render the label
buf.set_span(label_col, label_row, &label, clamped_label_width);
buf.set_span(label_col, label_row, label, clamped_label_width);
}
}

Expand Down
27 changes: 16 additions & 11 deletions src/widgets/paragraph.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use unicode_width::UnicodeWidthStr;

use super::block::BlockExt;
use crate::{
prelude::*,
text::StyledGrapheme,
Expand Down Expand Up @@ -280,19 +281,23 @@ impl<'a> Paragraph<'a> {
}
}

impl<'a> Widget for Paragraph<'a> {
fn render(mut self, area: Rect, buf: &mut Buffer) {
impl Widget for Paragraph<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
(&self).render(area, buf);
}
}

impl Widget for &Paragraph<'_> {
fn render(self, mut area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style);
let text_area = match self.block.take() {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
inner_area
}
None => area,
};
self.block.render(&mut area, buf);
self.render_paragraph(area, buf);
}
}

if text_area.height < 1 {
impl Paragraph<'_> {
fn render_paragraph(&self, text_area: Rect, buf: &mut Buffer) {
if text_area.is_empty() {
return;
}

Expand Down
27 changes: 16 additions & 11 deletions src/widgets/sparkline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::cmp::min;

use strum::{Display, EnumString};

use super::block::BlockExt;
use crate::{
prelude::*,
widgets::{Block, Widget},
Expand Down Expand Up @@ -156,18 +157,22 @@ impl<'a> Styled for Sparkline<'a> {
}
}

impl<'a> Widget for Sparkline<'a> {
fn render(mut self, area: Rect, buf: &mut Buffer) {
let spark_area = match self.block.take() {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
inner_area
}
None => area,
};
impl Widget for Sparkline<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
(&self).render(area, buf);
}
}

impl Widget for &Sparkline<'_> {
fn render(self, mut area: Rect, buf: &mut Buffer) {
self.block.render(&mut area, buf);
self.render_sparkline(area, buf);
}
}

if spark_area.height < 1 {
impl Sparkline<'_> {
fn render_sparkline(&self, spark_area: Rect, buf: &mut Buffer) {
if spark_area.is_empty() {
return;
}

Expand Down

0 comments on commit 45146ce

Please sign in to comment.