Skip to content

Commit

Permalink
Add separate text box placeholder for focus
Browse files Browse the repository at this point in the history
  • Loading branch information
LucasPickering committed Dec 2, 2024
1 parent 377d3f1 commit 8cd971c
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 9 deletions.
59 changes: 57 additions & 2 deletions crates/tui/src/view/common/text_box.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ const DEBOUNCE: Duration = Duration::from_millis(500);
pub struct TextBox {
// Parameters
sensitive: bool,
/// Text to show when text content is empty
placeholder_text: String,
/// Text to show when text content is empty and text box is in focus. If
/// `None`, the default placeholder will be shown instead.
placeholder_focused: Option<String>,
/// Predicate function to apply visual validation effect
#[debug(skip)]
validator: Option<Validator>,
Expand Down Expand Up @@ -77,6 +81,16 @@ impl TextBox {
self
}

/// Set placeholder text to show only while the text box is focused. If not
/// set, this will fallback to the general placeholder text.
pub fn placeholder_focused(
mut self,
placeholder: impl Into<String>,
) -> Self {
self.placeholder_focused = Some(placeholder.into());
self
}

/// Set validation function. If input is invalid, the `on_change` and
/// `on_submit` callbacks will be blocked, meaning the user must fix the
/// error or cancel.
Expand All @@ -87,6 +101,7 @@ impl TextBox {
self.validator = Some(Box::new(validator));
self
}

/// Set the callback to be called when the user clicks the textbox
pub fn on_click(mut self, f: impl 'static + Fn()) -> Self {
self.on_click = Some(Box::new(f));
Expand Down Expand Up @@ -261,12 +276,20 @@ impl Draw for TextBox {
fn draw(&self, frame: &mut Frame, _: (), metadata: DrawMetadata) {
let styles = &TuiContext::get().styles;

// Hide top secret data
let text: Text = if self.state.text.is_empty() {
Line::from(self.placeholder_text.as_str())
// Users can optionally set a different placeholder for when focused
let placeholder = if metadata.has_focus() {
self.placeholder_focused
.as_deref()
.unwrap_or(&self.placeholder_text)
} else {
&self.placeholder_text
};
Line::from(placeholder)
.style(styles.text_box.placeholder)
.into()
} else if self.sensitive {
// Hide top secret data
Masked::new(&self.state.text, '•').into()
} else {
self.state.text.as_str().into()
Expand Down Expand Up @@ -646,6 +669,38 @@ mod tests {
]]);
}

#[rstest]
fn test_placeholder_focused(
harness: TestHarness,
#[with(9, 1)] terminal: TestTerminal,
) {
let mut component = TestComponent::new(
&harness,
&terminal,
TextBox::default()
.placeholder("unfocused")
.placeholder_focused("focused"),
(),
);
let styles = &TuiContext::get().styles.text_box;

// Focused
assert_state(&component.data().state, "", 0);
terminal.assert_buffer_lines([vec![
cursor("f"),
Span::styled("ocused", styles.text.patch(styles.placeholder)),
text(" "),
]]);

// Unfocused
component.unfocus();
component.drain_draw().assert_empty();
terminal.assert_buffer_lines([vec![Span::styled(
"unfocused",
styles.text.patch(styles.placeholder),
)]]);
}

#[rstest]
fn test_validator(
harness: TestHarness,
Expand Down
14 changes: 9 additions & 5 deletions crates/tui/src/view/component/queryable_body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ impl QueryableBody {
move || ViewContext::push_event(Event::new_local(callback))
};
let text_box = TextBox::default()
.placeholder(format!("'{binding}' to filter body with JSONPath"))
.placeholder(format!("{binding} to query body"))
.placeholder_focused("Query with JSONPath (ex: $.results)")
.validator(|text| JsonPath::parse(text).is_ok())
// Callback trigger an events, so we can modify our own state
.on_click(send_local(QueryCallback::Focus))
Expand Down Expand Up @@ -429,10 +430,13 @@ mod tests {
vec![gutter("2"), " \"greeting\": \"hello\"".into()],
vec![gutter("3"), " } ".into()],
vec![gutter(" "), " ".into()],
vec![Span::styled(
"'/' to filter body with JSONPath",
styles.text.patch(styles.placeholder),
)],
vec![
Span::styled(
"/ to query body",
styles.text.patch(styles.placeholder),
),
Span::styled(" ", styles.text),
],
]);

// Type something into the query box
Expand Down
22 changes: 20 additions & 2 deletions crates/tui/src/view/test_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ pub struct TestComponent<'term, T, Props> {
/// components since props typically just contain identifiers, references,
/// and primitives. Modify using [Self::set_props].
props: Props,
/// Should the component be given focus on the next draw? Defaults to
/// `true`
has_focus: bool,
}

impl<'term, Props, T> TestComponent<'term, T, Props>
Expand Down Expand Up @@ -68,6 +71,7 @@ where
area: terminal.area(),
component,
props: initial_props,
has_focus: true,
};
// Do an initial draw to set up state, then handle any triggered events
slf.draw();
Expand All @@ -88,6 +92,16 @@ where
self.props = props;
}

/// Enable focus for the next draw
pub fn focus(&mut self) {
self.has_focus = true;
}

/// Disable focus for the next draw
pub fn unfocus(&mut self) {
self.has_focus = false;
}

/// Get a reference to the wrapped component's inner data
pub fn data(&self) -> &T {
self.component.data()
Expand All @@ -103,8 +117,12 @@ where
/// use the same props from the last draw.
fn draw(&mut self) {
self.terminal.draw(|frame| {
self.component
.draw(frame, self.props.clone(), self.area, true)
self.component.draw(
frame,
self.props.clone(),
self.area,
self.has_focus,
)
});
}

Expand Down

0 comments on commit 8cd971c

Please sign in to comment.