Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/kas-view/src/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ impl<Key> Driver<Key, bool> for View {
type Widget = CheckBox<bool>;

fn make(&mut self, _: &Key) -> Self::Widget {
CheckBox::new(|_, data: &bool| *data).with_editable(false)
CheckBox::new(|_, data: &bool| *data).with_read_only(true)
}
fn set_key(&mut self, _: &mut Self::Widget, _: &Key) {
// CheckBox has no metadata that needs to be reset
Expand Down
46 changes: 25 additions & 21 deletions crates/kas-widgets/src/check_box.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ mod CheckBox {
pub struct CheckBox<A> {
core: widget_core!(),
state: bool,
editable: bool,
read_only: bool,
last_change: Option<Instant>,
state_fn: Box<dyn Fn(&ConfigCx, &A) -> bool + Send>,
on_toggle: Option<Box<dyn Fn(&mut EventCx, &A, bool) + Send>>,
Expand Down Expand Up @@ -91,7 +91,7 @@ mod CheckBox {
CheckBox {
core: Default::default(),
state: false,
editable: true,
read_only: false,
last_change: None,
state_fn: Box::new(state_fn),
on_toggle: None,
Expand Down Expand Up @@ -129,28 +129,32 @@ mod CheckBox {
CheckBox::new(state_fn).with_msg(msg_fn)
}

/// Set whether this widget is editable (inline)
/// Set whether this widget is read-only (inline)
#[inline]
#[must_use]
pub fn with_editable(mut self, editable: bool) -> Self {
self.editable = editable;
pub fn with_read_only(mut self, read_only: bool) -> Self {
self.read_only = read_only;
self
}

/// Get whether this widget is editable
/// Get whether this widget is read-only
#[inline]
pub fn is_editable(&self) -> bool {
self.editable
pub fn is_read_only(&self) -> bool {
self.read_only
}

/// Set whether this widget is editable
/// Set whether this widget is read-only
#[inline]
pub fn set_editable(&mut self, editable: bool) {
self.editable = editable;
pub fn set_read_only(&mut self, read_only: bool) {
self.read_only = read_only;
}

/// Toggle the check box
/// Toggle the check box, if not read-only
pub fn toggle(&mut self, cx: &mut EventCx, data: &A) {
if self.read_only {
return;
}

// Note: do not update self.state; that is the responsibility of update.
self.state = !self.state;
if let Some(f) = self.on_toggle.as_ref() {
Expand Down Expand Up @@ -298,24 +302,24 @@ mod CheckButton {
CheckButton::new(label, state_fn).with_msg(msg_fn)
}

/// Set whether this widget is editable (inline)
/// Set whether this widget is read-only (inline)
#[inline]
#[must_use]
pub fn editable(mut self, editable: bool) -> Self {
self.inner = self.inner.with_editable(editable);
pub fn read_only(mut self, read_only: bool) -> Self {
self.inner = self.inner.with_read_only(read_only);
self
}

/// Get whether this widget is editable
/// Get whether this widget is read-only
#[inline]
pub fn is_editable(&self) -> bool {
self.inner.is_editable()
pub fn is_read_only(&self) -> bool {
self.inner.is_read_only()
}

/// Set whether this widget is editable
/// Set whether this widget is read-only
#[inline]
pub fn set_editable(&mut self, editable: bool) {
self.inner.set_editable(editable);
pub fn set_read_only(&mut self, read_only: bool) {
self.inner.set_read_only(read_only);
}

fn direction(&self) -> Direction {
Expand Down
14 changes: 7 additions & 7 deletions crates/kas-widgets/src/dialog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

use crate::adapt::AdaptWidgetAny;
use crate::edit::Editor;
use crate::{AccessLabel, Button, EditBox, Filler, ScrollLabel, SelectableLabel};
use crate::{AccessLabel, Button, EditBox, Filler, ScrollLabel};
use kas::prelude::*;
use kas::runner::AppData;
use kas::text::format::FormattableText;
Expand All @@ -33,7 +33,7 @@ mod MessageBox {
pub struct MessageBox<T: FormattableText + 'static> {
core: widget_core!(),
#[widget]
label: SelectableLabel<T>,
label: ScrollLabel<T>,
#[widget]
button: Button<AccessLabel>,
}
Expand All @@ -43,7 +43,7 @@ mod MessageBox {
pub fn new(message: T) -> Self {
MessageBox {
core: Default::default(),
label: SelectableLabel::new(message),
label: ScrollLabel::new(message),
button: Button::label_msg("&Ok", MessageBoxOk),
}
}
Expand Down Expand Up @@ -101,7 +101,7 @@ mod AlertError {
parent: Id,
title: String,
#[widget]
label: SelectableLabel<T>,
label: ScrollLabel<T>,
#[widget]
details: ScrollLabel<String>,
#[widget]
Expand All @@ -122,7 +122,7 @@ mod AlertError {
core: Default::default(),
parent: Id::default(),
title: "Error".to_string(),
label: SelectableLabel::new(message),
label: ScrollLabel::new(message),
details: ScrollLabel::new(details),
ok: Button::label_msg("&Ok", ErrorResult),
}
Expand Down Expand Up @@ -194,7 +194,7 @@ mod AlertUnsaved {
parent: Id,
title: String,
#[widget]
label: SelectableLabel<T>,
label: ScrollLabel<T>,
#[widget]
save: Button<AccessLabel>,
#[widget]
Expand All @@ -210,7 +210,7 @@ mod AlertUnsaved {
core: Default::default(),
parent: Id::default(),
title: "Unsaved changes".to_string(),
label: SelectableLabel::new(message),
label: ScrollLabel::new(message),
save: Button::label_msg("&Save", UnsavedResult::Save),
discard: Button::label_msg("&Discard", UnsavedResult::Discard),
cancel: Button::label_msg("&Cancel", UnsavedResult::Cancel),
Expand Down
94 changes: 60 additions & 34 deletions crates/kas-widgets/src/edit/edit_box.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@
// You may obtain a copy of the License in the LICENSE-APACHE file or at:
// https://www.apache.org/licenses/LICENSE-2.0

//! The [`EditField`] and [`EditBox`] widgets, plus supporting items
//! The [`EditBox`] widget

use super::*;
use crate::edit::highlight::{Highlighter, Plain};
use crate::{ScrollBar, ScrollBarMsg};
use kas::event::Scroll;
use kas::event::components::ScrollComponent;
use kas::messages::{ReplaceSelectedText, SetValueText};
use kas::prelude::*;
use kas::theme::{FrameStyle, TextClass};
use std::fmt::{Debug, Display};
Expand All @@ -21,17 +20,43 @@ mod EditBox {
/// A text-edit box
///
/// A single- or multi-line editor for unformatted text.
/// See also notes on [`EditField`].
///
/// By default, the editor supports a single-line only;
/// [`Self::with_multi_line`] can be used to change this.
///
/// ### Event handling
///
/// This widget attempts to handle all standard text-editor input and scroll
/// events.
///
/// Key events for moving the edit cursor (e.g. arrow keys) are consumed
/// only if the edit cursor is moved while key events for adjusting or using
/// the selection (e.g. `Command::Copy` and `Command::Deselect`)
/// are consumed only when a selection exists. In contrast, key events for
/// inserting or deleting text are always consumed.
///
/// [`Command::Enter`] inserts a line break in multi-line mode, but in
/// single-line mode or if the <kbd>Shift</kbd> key is held it is treated
/// the same as [`Command::Activate`].
///
/// ### Performance and limitations
///
/// Text representation is via a single [`String`]. Edit operations are
/// `O(n)` where `n` is the length of text (with text layout algorithms
/// having greater cost than copying bytes in the backing [`String`]).
/// This isn't necessarily *slow*; when run with optimizations the type can
/// handle type-setting around 20kB of UTF-8 in under 10ms (with significant
/// scope for optimization, given that currently layout is re-run from
/// scratch on each key stroke). Regardless, this approach is not designed
/// to scale to handle large documents via a single `EditBox` widget.
///
/// ### Messages
///
/// [`SetValueText`] may be used to replace the entire text and
/// [`ReplaceSelectedText`] may be used to replace selected text when this
/// widget is [editable](Editor::is_editable). This triggers the action
/// handlers [`EditGuard::edit`] followed by [`EditGuard::activate`].
/// [`kas::messages::SetValueText`] may be used to replace the entire text
/// and [`kas::messages::ReplaceSelectedText`] may be used to replace
/// selected text when this widget is not [read-only](Editor::is_read_only).
/// Both add an item to the undo history and invoke the action handler
/// [`EditGuard::edit`].
///
/// [`kas::messages::SetScrollOffset`] may be used to set the scroll offset.
#[autoimpl(Debug where G: trait, H: trait)]
Expand All @@ -42,9 +67,10 @@ mod EditBox {
scroll: ScrollComponent,
// NOTE: inner is a Viewport which doesn't use update methods, therefore we don't call them.
#[widget]
inner: EditField<G, H>,
inner: EditBoxCore<G, H>,
#[widget(&())]
vert_bar: ScrollBar<kas::dir::Down>,
frame_style: FrameStyle,
frame_offset: Offset,
frame_size: Size,
frame_offset_ex_margin: Offset,
Expand All @@ -64,7 +90,7 @@ mod EditBox {
rules.append(bar_rules);
}

let frame_rules = cx.frame(FrameStyle::EditBox, axis);
let frame_rules = cx.frame(self.frame_style, axis);
self.frame_offset_ex_margin
.set_component(axis, frame_rules.size());
let (rules, offset, size) = frame_rules.surround(rules);
Expand Down Expand Up @@ -111,7 +137,7 @@ mod EditBox {
let mut draw_inner = draw.re();
draw_inner.set_id(self.inner.id());
let bg = self.inner.background_color();
draw_inner.frame(self.rect(), FrameStyle::EditBox, bg);
draw_inner.frame(self.rect(), self.frame_style, bg);

self.inner
.draw_with_offset(draw.re(), self.clip_rect, self.scroll.offset());
Expand Down Expand Up @@ -170,29 +196,18 @@ mod EditBox {
}

fn handle_messages(&mut self, cx: &mut EventCx<'_>, data: &G::Data) {
let action = if cx.last_child() == Some(widget_index![self.vert_bar])
let offset = if cx.last_child() == Some(widget_index![self.vert_bar])
&& let Some(ScrollBarMsg(y)) = cx.try_pop()
{
let offset = Offset(self.scroll.offset().0, y);
self.scroll.set_offset(offset)
Offset(self.scroll.offset().0, y)
} else if let Some(kas::messages::SetScrollOffset(offset)) = cx.try_pop() {
self.scroll.set_offset(offset)
} else if self.is_editable()
&& let Some(SetValueText(string)) = cx.try_pop()
{
self.edit(cx, data, |edit, cx| {
edit.pre_commit();
edit.set_string(cx, string);
});
return;
} else if let Some(&ReplaceSelectedText(_)) = cx.try_peek() {
self.inner.handle_messages(cx, data);
return;
offset
} else {
self.inner.handle_messages(cx, data);
return;
};

if let Some(moved) = action {
if let Some(moved) = self.scroll.set_offset(offset) {
cx.action_moved(moved);
self.update_scroll_offset(cx);
}
Expand Down Expand Up @@ -232,8 +247,9 @@ mod EditBox {
EditBox {
core: Default::default(),
scroll: Default::default(),
inner: EditField::new(guard),
inner: EditBoxCore::new(guard),
vert_bar: Default::default(),
frame_style: FrameStyle::EditBox,
frame_offset: Default::default(),
frame_size: Default::default(),
frame_offset_ex_margin: Default::default(),
Expand All @@ -254,6 +270,7 @@ mod EditBox {
scroll: self.scroll,
inner: self.inner.with_highlighter(highlighter),
vert_bar: self.vert_bar,
frame_style: self.frame_style,
frame_offset: self.frame_offset,
frame_size: self.frame_size,
frame_offset_ex_margin: self.frame_offset_ex_margin,
Expand All @@ -267,6 +284,15 @@ mod EditBox {
self.inner.set_highlighter(highlighter);
}

/// Replace the frame style
///
/// The default is [`FrameStyle::EditBox`].
#[inline]
pub fn with_frame_style(mut self, style: FrameStyle) -> Self {
self.frame_style = style;
self
}

fn update_content_size(&mut self, cx: &mut EventState) {
if !self.core.status.is_sized() {
return;
Expand Down Expand Up @@ -301,15 +327,15 @@ impl<A: 'static> EditBox<DefaultGuard<A>> {
#[inline]
pub fn text<S: ToString>(text: S) -> Self {
EditBox {
inner: EditField::text(text),
inner: EditBoxCore::text(text),
..Default::default()
}
}

/// Construct a read-only `EditBox` displaying some `String` value
#[inline]
pub fn string(value_fn: impl Fn(&A) -> String + Send + 'static) -> EditBox<StringGuard<A>> {
EditBox::new(StringGuard::new(value_fn)).with_editable(false)
EditBox::new(StringGuard::new(value_fn)).with_read_only(true)
}

/// Construct an `EditBox` for a parsable value (e.g. a number)
Expand Down Expand Up @@ -356,14 +382,14 @@ impl<A: 'static> EditBox<StringGuard<A>> {
/// The `msg_fn` is called when the field is activated (<kbd>Enter</kbd>)
/// and when it loses focus after content is changed.
///
/// This method sets self as editable (see [`Self::with_editable`]).
/// This method sets self as editable (see [`Self::with_read_only`]).
#[must_use]
pub fn with_msg<M>(mut self, msg_fn: impl Fn(&str) -> M + Send + 'static) -> Self
where
M: Debug + 'static,
{
self.inner.guard = self.inner.guard.with_msg(msg_fn);
self.inner = self.inner.with_editable(true);
self.inner = self.inner.with_read_only(false);
self
}
}
Expand All @@ -379,11 +405,11 @@ impl<G: EditGuard, H: Highlighter> EditBox<G, H> {
self
}

/// Set whether this widget is editable (inline)
/// Set whether this `EditBox` is read-only (inline)
#[inline]
#[must_use]
pub fn with_editable(mut self, editable: bool) -> Self {
self.inner = self.inner.with_editable(editable);
pub fn with_read_only(mut self, read_only: bool) -> Self {
self.inner = self.inner.with_read_only(read_only);
self
}

Expand Down
Loading