Skip to content

Commit

Permalink
Unify Prose and Textbox with an underlying TextRegion (#754)
Browse files Browse the repository at this point in the history
Remaining steps

- [x] Update Textbox to use this
- [x] Add internal padding

Follow-up work:
- [ ] Restore IME support
- [ ] Upstream new result of PlainEditor (once other steps are complete)
  • Loading branch information
DJMcNab authored Nov 21, 2024
1 parent 10dc9d1 commit d8d916c
Show file tree
Hide file tree
Showing 22 changed files with 1,438 additions and 1,364 deletions.
9 changes: 3 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,6 @@ env:
NO_WASM_PKGS: "--exclude masonry --exclude xilem"
# Only some of our examples support Android (primarily due to extra required boilerplate).
ANDROID_TARGETS: "-p xilem --example mason_android --example calc_android --example stopwatch_android --example variable_clock_android --example http_cats_android --example to_do_mvc_android"
# We do not run the masonry snapshot tests, because those currently require a specific font stack
# See https://github.com/linebender/xilem/pull/233
SKIP_RENDER_SNAPSHOTS: 1
# We do not run the masonry render tests, because those require Vello rendering to be working
# See also https://github.com/linebender/vello/pull/610
SKIP_RENDER_TESTS: 1


# Rationale
Expand Down Expand Up @@ -259,6 +253,9 @@ jobs:
- name: cargo nextest
run: cargo nextest run --workspace --locked --all-features --no-fail-fast
env:
# We do not run the masonry render tests on platforms without a working GPU,
# because those require Vello rendering to be working
# See also https://github.com/linebender/vello/pull/610
SKIP_RENDER_TESTS: ${{ matrix.skip_gpu }}

- name: Upload test results due to failure
Expand Down
8 changes: 4 additions & 4 deletions masonry/examples/grid_masonry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

use masonry::dpi::LogicalSize;
use masonry::text::StyleProperty;
use masonry::widget::{Button, Grid, GridParams, Prose, RootWidget, SizedBox};
use masonry::widget::{Button, Grid, GridParams, Prose, RootWidget, SizedBox, TextArea};
use masonry::{Action, AppDriver, Color, DriverCtx, PointerButton, WidgetId};
use parley::layout::Alignment;
use winit::window::Window;
Expand Down Expand Up @@ -43,11 +43,11 @@ fn grid_button(params: GridParams) -> Button {
}

fn main() {
let label = SizedBox::new(
Prose::new("Change spacing by right and left clicking on the buttons")
let label = SizedBox::new(Prose::from_text_area(
TextArea::new_immutable("Change spacing by right and left clicking on the buttons")
.with_style(StyleProperty::FontSize(14.0))
.with_alignment(Alignment::Middle),
)
))
.border(Color::rgb8(40, 40, 80), 1.0);
let button_inputs = vec![
GridParams {
Expand Down
5 changes: 3 additions & 2 deletions masonry/examples/to_do_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
#![expect(elided_lifetimes_in_paths, reason = "Deferred: Noisy")]

use masonry::dpi::LogicalSize;
use masonry::widget::{Button, Flex, Label, Portal, RootWidget, Textbox, WidgetMut};
use masonry::widget::{Button, Flex, Label, Portal, RootWidget, TextArea, Textbox, WidgetMut};
use masonry::{Action, AppDriver, DriverCtx, WidgetId};
use winit::window::Window;

Expand All @@ -32,7 +32,8 @@ impl AppDriver for Driver {
let mut first_row = first_row.downcast::<Flex>();
let mut textbox = Flex::child_mut(&mut first_row, 0).unwrap();
let mut textbox = textbox.downcast::<Textbox>();
Textbox::reset_text(&mut textbox, String::new());
let mut text_area = Textbox::text_mut(&mut textbox);
TextArea::reset_text(&mut text_area, "");
}
Action::TextChanged(new_text) => {
self.next_task = new_text.clone();
Expand Down
6 changes: 4 additions & 2 deletions masonry/examples/two_textboxes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ impl AppDriver for Driver {

fn main() {
let main_widget = Flex::column()
.gap(0.0)
.with_spacer(VERTICAL_WIDGET_SPACING)
.with_child(Textbox::new(""))
.with_child(Textbox::new(""))
.with_spacer(VERTICAL_WIDGET_SPACING);
.with_spacer(VERTICAL_WIDGET_SPACING)
.with_child(Textbox::new(""));

let window_size = LogicalSize::new(400.0, 400.0);
let window_attributes = Window::default_attributes()
Expand Down
140 changes: 89 additions & 51 deletions masonry/src/text/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ use parley::{
cursor::{Cursor, Selection, VisualMode},
Affinity, Alignment, Layout, Line,
},
style::{Brush, StyleProperty},
style::Brush,
FontContext, LayoutContext, Rect,
};
use std::{borrow::ToOwned, string::String, sync::Arc, vec::Vec};
use std::{borrow::ToOwned, string::String, vec::Vec};

use super::styleset::StyleSet;

#[derive(Copy, Clone, Debug)]
pub enum ActiveText<'a> {
Expand Down Expand Up @@ -50,7 +52,7 @@ pub struct PlainEditor<T>
where
T: Brush + Clone + Debug + PartialEq + Default,
{
default_style: Arc<[StyleProperty<'static, T>]>,
default_style: StyleSet<T>,
buffer: String,
layout: Layout<T>,
layout_access: LayoutAccessibility,
Expand All @@ -72,22 +74,21 @@ where
generation: Generation,
}

// TODO: When MSRV >= 1.80 we can remove this. Default was not implemented for Arc<[T]> where T: !Default until 1.80
impl<T> Default for PlainEditor<T>
impl<T> PlainEditor<T>
where
T: Brush + Clone + Debug + PartialEq + Default,
T: Brush,
{
fn default() -> Self {
pub fn new(font_size: f32) -> Self {
Self {
default_style: Arc::new([]),
default_style: StyleSet::new(font_size),
buffer: Default::default(),
layout: Default::default(),
layout_access: Default::default(),
selection: Default::default(),
cursor_mode: Default::default(),
width: Default::default(),
width: None,
scale: 1.0,
layout_dirty: Default::default(),
layout_dirty: false,
alignment: Alignment::Start,
// We don't use the `default` value to start with, as our consumers
// will choose to use that as their initial value, but will probably need
Expand All @@ -112,37 +113,7 @@ impl<T> PlainEditorTxn<'_, T>
where
T: Brush + Clone + Debug + PartialEq + Default,
{
/// Replace the whole text buffer.
pub fn set_text(&mut self, is: &str) {
self.editor.buffer.clear();
self.editor.buffer.push_str(is);
self.editor.layout_dirty = true;
}

/// Set the width of the layout.
pub fn set_width(&mut self, width: Option<f32>) {
self.editor.width = width;
self.editor.layout_dirty = true;
}

/// Set the alignment of the layout.
pub fn set_alignment(&mut self, alignment: Alignment) {
self.editor.alignment = alignment;
self.editor.layout_dirty = true;
}

/// Set the scale for the layout.
pub fn set_scale(&mut self, scale: f32) {
self.editor.scale = scale;
self.editor.layout_dirty = true;
}

/// Set the default style for the layout.
pub fn set_default_style(&mut self, style: Arc<[StyleProperty<'static, T>]>) {
self.editor.default_style = style;
self.editor.layout_dirty = true;
}

// --- MARK: Forced relayout ---
/// Insert at cursor, or replace selection.
pub fn insert_or_replace_selection(&mut self, s: &str) {
self.editor
Expand Down Expand Up @@ -239,6 +210,7 @@ where
}
}

// --- MARK: Cursor Movement ---
/// Move the cursor to the cluster boundary nearest this point in the layout.
pub fn move_to_point(&mut self, x: f32, y: f32) {
self.refresh_layout();
Expand Down Expand Up @@ -497,6 +469,7 @@ where
}
}

// --- MARK: Internal helpers ---
fn update_layout(&mut self) {
self.editor.update_layout(self.font_cx, self.layout_cx);
}
Expand All @@ -510,26 +483,39 @@ impl<T> PlainEditor<T>
where
T: Brush + Clone + Debug + PartialEq + Default,
{
/// Run a series of [`PlainEditorTxn`] methods, updating the layout
/// if necessary.
/// Run a series of [`PlainEditorTxn`] methods.
///
/// This is a utility shorthand around [`transaction`](Self::transaction);
pub fn transact<R>(
&mut self,
font_cx: &mut FontContext,
layout_cx: &mut LayoutContext<T>,
callback: impl FnOnce(&mut PlainEditorTxn<'_, T>) -> R,
) -> R {
let mut txn = PlainEditorTxn {
let mut txn = self.transaction(font_cx, layout_cx);
callback(&mut txn)
}

/// Run a series of [`PlainEditorTxn`] methods, updating the layout
/// if necessary.
///
/// This is a utility shorthand to simplify methods which require the editor
/// and the provided contexts.
pub fn transaction<'txn>(
&'txn mut self,
font_cx: &'txn mut FontContext,
layout_cx: &'txn mut LayoutContext<T>,
) -> PlainEditorTxn<'txn, T> {
PlainEditorTxn {
editor: self,
font_cx,
layout_cx,
};
let ret = callback(&mut txn);
txn.update_layout();
ret
}
}

/// Make a cursor at a given byte index
fn cursor_at(&self, index: usize) -> Cursor {
// TODO: Do we need to be non-dirty?
// FIXME: `Selection` should make this easier
if index >= self.buffer.len() {
Cursor::from_index(
Expand All @@ -548,6 +534,7 @@ where
layout_cx: &mut LayoutContext<T>,
s: &str,
) {
// TODO: Do we need to be non-dirty?
let range = self.selection.text_range();
let start = range.start;
if self.selection.is_collapsed() {
Expand Down Expand Up @@ -618,11 +605,62 @@ where
self.generation
}

/// Get the full read-only details from the layout.
pub fn layout(&self) -> &Layout<T> {
/// Get the full read-only details from the layout
pub fn layout(
&mut self,
font_cx: &mut FontContext,
layout_cx: &mut LayoutContext<T>,
) -> &Layout<T> {
self.refresh_layout(font_cx, layout_cx);
&self.layout
}

/// Get the full read-only details from the layout, if valid.
pub fn get_layout(&self) -> Option<&Layout<T>> {
if self.layout_dirty {
None
} else {
Some(&self.layout)
}
}

/// Get the (potentially invalid) details from the layout.
pub fn layout_raw(&self) -> &Layout<T> {
&self.layout
}

/// Replace the whole text buffer.
pub fn set_text(&mut self, is: &str) {
self.buffer.clear();
self.buffer.push_str(is);
self.layout_dirty = true;
}

/// Set the width of the layout.
// TODO: If this is infinite, is the width used for alignnment the min width?
pub fn set_width(&mut self, width: Option<f32>) {
self.width = width;
self.layout_dirty = true;
}

/// Set the alignment of the layout.
pub fn set_alignment(&mut self, alignment: Alignment) {
self.alignment = alignment;
self.layout_dirty = true;
}

/// Set the scale for the layout.
pub fn set_scale(&mut self, scale: f32) {
self.scale = scale;
self.layout_dirty = true;
}

/// Set the default style for the layout.
pub fn edit_styles(&mut self) -> &mut StyleSet<T> {
self.layout_dirty = true;
&mut self.default_style
}

/// Update the layout if it is dirty.
fn refresh_layout(&mut self, font_cx: &mut FontContext, layout_cx: &mut LayoutContext<T>) {
if self.layout_dirty {
Expand All @@ -633,7 +671,7 @@ where
/// Update the layout.
fn update_layout(&mut self, font_cx: &mut FontContext, layout_cx: &mut LayoutContext<T>) {
let mut builder = layout_cx.ranged_builder(font_cx, &self.buffer, self.scale);
for prop in self.default_style.iter() {
for prop in self.default_style.inner().values() {
builder.push_default(prop.to_owned());
}
builder.build_into(&mut self.layout, &self.buffer);
Expand Down
31 changes: 2 additions & 29 deletions masonry/src/text/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@
mod editor;
mod render_text;

use std::{collections::HashMap, mem::Discriminant};
mod styleset;

pub use editor::{ActiveText, Generation, PlainEditor, PlainEditorTxn};
pub use render_text::render_text;
Expand All @@ -29,30 +28,4 @@ pub struct BrushIndex(pub usize);

pub type StyleProperty = parley::StyleProperty<'static, BrushIndex>;

/// A set of Parley styles.
pub struct StyleSet(HashMap<Discriminant<StyleProperty>, StyleProperty>);

impl StyleSet {
pub fn new(font_size: f32) -> Self {
let mut this = Self(Default::default());
this.insert(StyleProperty::FontSize(font_size));
this
}

pub fn insert(&mut self, style: StyleProperty) -> Option<StyleProperty> {
let discriminant = std::mem::discriminant(&style);
self.0.insert(discriminant, style)
}

pub fn retain(&mut self, mut f: impl FnMut(&StyleProperty) -> bool) {
self.0.retain(|_, v| f(v));
}

pub fn remove(&mut self, property: Discriminant<StyleProperty>) -> Option<StyleProperty> {
self.0.remove(&property)
}

pub fn inner(&self) -> &HashMap<Discriminant<StyleProperty>, StyleProperty> {
&self.0
}
}
pub type StyleSet = styleset::StyleSet<BrushIndex>;
40 changes: 40 additions & 0 deletions masonry/src/text/styleset.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright 2018 the Xilem Authors and the Druid Authors
// SPDX-License-Identifier: Apache-2.0

use std::{collections::HashMap, mem::Discriminant};

type StyleProperty<Brush> = parley::StyleProperty<'static, Brush>;

/// A set of Parley styles.
#[derive(Clone, Debug)]
pub struct StyleSet<Brush: parley::Brush>(
HashMap<Discriminant<StyleProperty<Brush>>, StyleProperty<Brush>>,
);

impl<Brush: parley::Brush> StyleSet<Brush> {
pub fn new(font_size: f32) -> Self {
let mut this = Self(Default::default());
this.insert(StyleProperty::FontSize(font_size));
this
}

pub fn insert(&mut self, style: StyleProperty<Brush>) -> Option<StyleProperty<Brush>> {
let discriminant = std::mem::discriminant(&style);
self.0.insert(discriminant, style)
}

pub fn retain(&mut self, mut f: impl FnMut(&StyleProperty<Brush>) -> bool) {
self.0.retain(|_, v| f(v));
}

pub fn remove(
&mut self,
property: Discriminant<StyleProperty<Brush>>,
) -> Option<StyleProperty<Brush>> {
self.0.remove(&property)
}

pub fn inner(&self) -> &HashMap<Discriminant<StyleProperty<Brush>>, StyleProperty<Brush>> {
&self.0
}
}
Loading

0 comments on commit d8d916c

Please sign in to comment.