druid 0.8.2

Data-oriented Rust UI design toolkit.
Documentation
// Copyright 2021 The Druid Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! A textbox that that parses and validates data.

use tracing::instrument;

use super::TextBox;
use crate::debug_state::DebugState;
use crate::text::{Formatter, Selection, TextComponent, ValidationError};
use crate::widget::prelude::*;
use crate::{Data, Selector};

const BEGIN_EDITING: Selector = Selector::new("druid.builtin.textbox-begin-editing");
const COMPLETE_EDITING: Selector = Selector::new("druid.builtin.textbox-complete-editing");

/// A `TextBox` that uses a [`Formatter`] to handle formatting and validation
/// of its data.
///
/// There are a number of ways to customize the behaviour of the text box
/// in relation to the provided [`Formatter`]:
///
/// - [`ValueTextBox::validate_while_editing`] takes a flag that determines whether
/// or not the textbox can display text that is not valid, while editing is
/// in progress. (Text will still be validated when the user attempts to complete
/// editing.)
///
/// - [`ValueTextBox::update_data_while_editing`] takes a flag that determines
/// whether the output value is updated during editing, when possible.
///
/// - [`ValueTextBox::delegate`] allows you to provide some implementation of
/// the [`ValidationDelegate`] trait, which receives a callback during editing;
/// this can be used to report errors further back up the tree.
pub struct ValueTextBox<T> {
    child: TextBox<String>,
    formatter: Box<dyn Formatter<T>>,
    callback: Option<Box<dyn ValidationDelegate>>,
    is_editing: bool,
    validate_while_editing: bool,
    update_data_while_editing: bool,
    /// the last data that this textbox saw or created.
    /// This is used to determine when a change to the data is originating
    /// elsewhere in the application, which we need to special-case
    last_known_data: Option<T>,
    force_selection: Option<Selection>,
    old_buffer: String,
    buffer: String,
}

/// A type that can be registered to receive callbacks as the state of a
/// [`ValueTextBox`] changes.
pub trait ValidationDelegate {
    /// Called with a [`TextBoxEvent`] whenever the validation state of a
    /// [`ValueTextBox`] changes.
    fn event(&mut self, ctx: &mut EventCtx, event: TextBoxEvent, current_text: &str);
}

/// Events sent to a [`ValidationDelegate`].
pub enum TextBoxEvent {
    /// The textbox began editing.
    Began,
    /// An edit occurred which was considered valid by the [`Formatter`].
    Changed,
    /// An edit occurred which was rejected by the [`Formatter`].
    PartiallyInvalid(ValidationError),
    /// The user attempted to finish editing, but the input was not valid.
    Invalid(ValidationError),
    /// The user finished editing, with valid input.
    Complete,
    /// Editing was cancelled.
    Cancel,
}

impl TextBox<String> {
    /// Turn this `TextBox` into a [`ValueTextBox`], using the [`Formatter`] to
    /// manage the value.
    ///
    /// For simple value formatting, you can use the [`ParseFormatter`].
    ///
    /// [`Formatter`]: crate::text::Formatter
    /// [`ParseFormatter`]: crate::text::ParseFormatter
    pub fn with_formatter<T: Data>(
        self,
        formatter: impl Formatter<T> + 'static,
    ) -> ValueTextBox<T> {
        ValueTextBox::new(self, formatter)
    }
}

impl<T: Data> ValueTextBox<T> {
    /// Create a new `ValueTextBox` from a normal [`TextBox`] and a [`Formatter`].
    ///
    /// [`TextBox`]: super::TextBox
    /// [`Formatter`]: crate::text::Formatter
    pub fn new(mut child: TextBox<String>, formatter: impl Formatter<T> + 'static) -> Self {
        child.text_mut().borrow_mut().send_notification_on_return = true;
        child.text_mut().borrow_mut().send_notification_on_cancel = true;
        child.handles_tab_notifications = false;
        ValueTextBox {
            child,
            formatter: Box::new(formatter),
            callback: None,
            is_editing: false,
            last_known_data: None,
            validate_while_editing: true,
            update_data_while_editing: false,
            old_buffer: String::new(),
            buffer: String::new(),
            force_selection: None,
        }
    }

    /// Builder-style method to set an optional [`ValidationDelegate`] on this
    /// textbox.
    pub fn delegate(mut self, delegate: impl ValidationDelegate + 'static) -> Self {
        self.callback = Some(Box::new(delegate));
        self
    }

    /// Builder-style method to set whether or not this text box validates
    /// its contents during editing.
    ///
    /// If `true` (the default) edits that fail validation
    /// ([`Formatter::validate_partial_input`]) will be rejected. If `false`,
    /// those edits will be accepted, and the text box will be updated.
    pub fn validate_while_editing(mut self, validate: bool) -> Self {
        self.validate_while_editing = validate;
        self
    }

    /// Builder-style method to set whether or not this text box updates the
    /// incoming data during editing.
    ///
    /// If `false` (the default) the data is only updated when editing completes.
    pub fn update_data_while_editing(mut self, flag: bool) -> Self {
        self.update_data_while_editing = flag;
        self
    }

    fn complete(&mut self, ctx: &mut EventCtx, data: &mut T) -> bool {
        match self.formatter.value(&self.buffer) {
            Ok(new_data) => {
                *data = new_data;
                self.buffer = self.formatter.format(data);
                self.is_editing = false;
                ctx.request_update();
                self.send_event(ctx, TextBoxEvent::Complete);
                true
            }
            Err(err) => {
                if self.child.text().can_write() {
                    if let Some(inval) = self
                        .child
                        .text_mut()
                        .borrow_mut()
                        .set_selection(Selection::new(0, self.buffer.len()))
                    {
                        ctx.invalidate_text_input(inval);
                    }
                }
                self.send_event(ctx, TextBoxEvent::Invalid(err));
                // our content isn't valid
                // ideally we would flash the background or something
                false
            }
        }
    }

    fn cancel(&mut self, ctx: &mut EventCtx, data: &T) {
        self.is_editing = false;
        self.buffer = self.formatter.format(data);
        ctx.request_update();
        ctx.resign_focus();
        self.send_event(ctx, TextBoxEvent::Cancel);
    }

    fn begin(&mut self, ctx: &mut EventCtx, data: &T) {
        self.is_editing = true;
        self.buffer = self.formatter.format_for_editing(data);
        self.last_known_data = Some(data.clone());
        ctx.request_update();
        self.send_event(ctx, TextBoxEvent::Began);
    }

    fn send_event(&mut self, ctx: &mut EventCtx, event: TextBoxEvent) {
        if let Some(delegate) = self.callback.as_mut() {
            delegate.event(ctx, event, &self.buffer)
        }
    }
}

impl<T: Data + std::fmt::Debug> Widget<T> for ValueTextBox<T> {
    #[instrument(
        name = "ValueTextBox",
        level = "trace",
        skip(self, ctx, event, data, env)
    )]
    fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) {
        if matches!(event, Event::Command(cmd) if cmd.is(BEGIN_EDITING)) {
            return self.begin(ctx, data);
        }

        if self.is_editing {
            // if we reject an edit we want to reset the selection
            let pre_sel = if self.child.text().can_read() {
                Some(self.child.text().borrow().selection())
            } else {
                None
            };
            match event {
                // this is caused by an external focus change, like the mouse being clicked
                // elsewhere.
                Event::Command(cmd) if cmd.is(COMPLETE_EDITING) => {
                    if !self.complete(ctx, data) {
                        self.cancel(ctx, data);
                    }
                    return;
                }
                Event::Notification(cmd) if cmd.is(TextComponent::TAB) => {
                    ctx.set_handled();
                    ctx.request_paint();
                    if self.complete(ctx, data) {
                        ctx.focus_next();
                    }
                    return;
                }
                Event::Notification(cmd) if cmd.is(TextComponent::BACKTAB) => {
                    ctx.request_paint();
                    ctx.set_handled();
                    if self.complete(ctx, data) {
                        ctx.focus_prev();
                    }
                    return;
                }
                Event::Notification(cmd) if cmd.is(TextComponent::RETURN) => {
                    ctx.set_handled();
                    if self.complete(ctx, data) {
                        ctx.resign_focus();
                    }
                    return;
                }
                Event::Notification(cmd) if cmd.is(TextComponent::CANCEL) => {
                    ctx.set_handled();
                    self.cancel(ctx, data);
                    return;
                }
                event => {
                    self.child.event(ctx, event, &mut self.buffer, env);
                }
            }
            // if an edit occurred, validate it with the formatter
            // notifications can arrive before update, so we always ignore them
            if !matches!(event, Event::Notification(_)) && self.buffer != self.old_buffer {
                let mut validation = self
                    .formatter
                    .validate_partial_input(&self.buffer, &self.child.text().borrow().selection());

                if self.validate_while_editing {
                    let new_buf = match (validation.text_change.take(), validation.is_err()) {
                        (Some(new_text), _) => {
                            // be helpful: if the formatter is misbehaved, log it.
                            if self
                                .formatter
                                .validate_partial_input(&new_text, &Selection::caret(0))
                                .is_err()
                            {
                                tracing::warn!(
                                    "formatter replacement text does not validate: '{}'",
                                    &new_text
                                );
                                None
                            } else {
                                Some(new_text)
                            }
                        }
                        (None, true) => Some(self.old_buffer.clone()),
                        _ => None,
                    };

                    let new_sel = match (validation.selection_change.take(), validation.is_err()) {
                        (Some(new_sel), _) => Some(new_sel),
                        (None, true) if pre_sel.is_some() => pre_sel,
                        _ => None,
                    };

                    if let Some(new_buf) = new_buf {
                        self.buffer = new_buf;
                    }

                    self.force_selection = new_sel;

                    if self.update_data_while_editing && !validation.is_err() {
                        if let Ok(new_data) = self.formatter.value(&self.buffer) {
                            *data = new_data;
                            self.last_known_data = Some(data.clone());
                        }
                    }
                }

                match validation.error() {
                    Some(err) => {
                        self.send_event(ctx, TextBoxEvent::PartiallyInvalid(err.to_owned()))
                    }
                    None => self.send_event(ctx, TextBoxEvent::Changed),
                };
                ctx.request_update();
            }
        // if we *aren't* editing:
        } else {
            if let Event::MouseDown(_) = event {
                self.begin(ctx, data);
            }
            self.child.event(ctx, event, &mut self.buffer, env);
        }
    }

    #[instrument(
        name = "ValueTextBox",
        level = "trace",
        skip(self, ctx, event, data, env)
    )]
    fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) {
        match event {
            LifeCycle::WidgetAdded => {
                self.buffer = self.formatter.format(data);
                self.old_buffer = self.buffer.clone();
            }
            LifeCycle::FocusChanged(true) if !self.is_editing => {
                ctx.submit_command(BEGIN_EDITING.to(ctx.widget_id()));
            }
            LifeCycle::FocusChanged(false) => {
                ctx.submit_command(COMPLETE_EDITING.to(ctx.widget_id()));
            }
            _ => (),
        }
        self.child.lifecycle(ctx, event, &self.buffer, env);
    }

    #[instrument(
        name = "ValueTextBox",
        level = "trace",
        skip(self, ctx, old, data, env)
    )]
    fn update(&mut self, ctx: &mut UpdateCtx, old: &T, data: &T, env: &Env) {
        if let Some(sel) = self.force_selection.take() {
            if self.child.text().can_write() {
                if let Some(change) = self.child.text_mut().borrow_mut().set_selection(sel) {
                    ctx.invalidate_text_input(change);
                }
            }
        }
        let changed_by_us = self
            .last_known_data
            .as_ref()
            .map(|d| d.same(data))
            .unwrap_or(false);
        if self.is_editing {
            if changed_by_us {
                self.child.update(ctx, &self.old_buffer, &self.buffer, env);
                self.old_buffer = self.buffer.clone();
            } else {
                // textbox is not well equipped to deal with the fact that, in
                // Druid, data can change anywhere in the tree. If we are actively
                // editing, and new data arrives, we ignore the new data and keep
                // editing; the alternative would be to cancel editing, which
                // could also make sense.
                tracing::warn!(
                    "ValueTextBox data changed externally, idk: '{}'",
                    self.formatter.format(data)
                );
            }
        } else {
            if !old.same(data) {
                // we aren't editing and data changed
                let new_text = self.formatter.format(data);
                // it's possible for different data inputs to produce the same formatted
                // output, in which case we would overwrite our actual previous data
                if !new_text.same(&self.buffer) {
                    self.old_buffer = std::mem::replace(&mut self.buffer, new_text);
                }
            }

            if !self.old_buffer.same(&self.buffer) {
                // child widget handles calling request_layout, as needed
                self.child.update(ctx, &self.old_buffer, &self.buffer, env);
                self.old_buffer = self.buffer.clone();
            } else if ctx.env_changed() {
                self.child.update(ctx, &self.buffer, &self.buffer, env);
            }
        }
    }

    #[instrument(
        name = "ValueTextBox",
        level = "trace",
        skip(self, ctx, bc, _data, env)
    )]
    fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, _data: &T, env: &Env) -> Size {
        self.child.layout(ctx, bc, &self.buffer, env)
    }

    #[instrument(name = "ValueTextBox", level = "trace", skip(self, ctx, _data, env))]
    fn paint(&mut self, ctx: &mut PaintCtx, _data: &T, env: &Env) {
        self.child.paint(ctx, &self.buffer, env);
    }

    fn debug_state(&self, _data: &T) -> DebugState {
        DebugState {
            display_name: self.short_type_name().to_string(),
            main_value: self.buffer.clone(),
            ..Default::default()
        }
    }
}