Skip to content

Commit

Permalink
refactor(core): 💡 simplify the structure of the text editing widget
Browse files Browse the repository at this point in the history
  • Loading branch information
M-Adoo authored and rchangelog[bot] committed Jan 25, 2025
1 parent 1060445 commit e87cb7f
Show file tree
Hide file tree
Showing 15 changed files with 553 additions and 628 deletions.
20 changes: 10 additions & 10 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,21 @@ Please only add new entries below the [Unreleased](#unreleased---releasedate) he
### Features

- **core**: Add builtin field `clip_boundary`. (#694 @wjian23)
- **core**: `IgnorePointer` now has the ability to only ignore events for the widget itself. (#pr @M-Adoo)
- **core**: Included `BoxPainter` to draw decorations starting from the widget box's origin while ignoring padding. (#pr @M-Adoo)
- **core**: `IgnorePointer` now has the ability to only ignore events for the widget itself. (#695 @M-Adoo)
- **core**: Included `BoxPainter` to draw decorations starting from the widget box's origin while ignoring padding. (#695 @M-Adoo)

### Changed

- **macros**: Generate cleaner code for #[derive(Declare)] when all fields are omitted. (#pr @M-Adoo)
- **macros**: Generate cleaner code for #[derive(Declare)] when all fields are omitted. (#695 @M-Adoo)

### Fixed

- **core & widgets**: Layouts are not permitted to return an infinite size, so if a layout requires scaling or expanding the size infinitely, that action should be disregarded. (#pr @M-Adoo)
- **macros**: Embedding `fn_widget!` may lead to missed captured variables. (#pr @M-Adoo)
- **core**: The child should not be painted when visible is false. (#pr @M-Adoo)
- **core**: Ensure that the content widget size in the scrollable widget is not smaller than its viewport. (#pr @M-Adoo)
- **core**: The crash occurs when a parent widget with a class tries to convert the widget with more than one leaf. (#pr @M-Adoo)
- **core**: The padding only reduces the content size and does not affect the border and background size. (#pr @M-Adoo)
- **core & widgets**: Layouts are not permitted to return an infinite size, so if a layout requires scaling or expanding the size infinitely, that action should be disregarded. (#695 @M-Adoo)
- **macros**: Embedding `fn_widget!` may lead to missed captured variables. (#695 @M-Adoo)
- **core**: The child should not be painted when visible is false. (#695 @M-Adoo)
- **core**: Ensure that the content widget size in the scrollable widget is not smaller than its viewport. (#695 @M-Adoo)
- **core**: The crash occurs when a parent widget with a class tries to convert the widget with more than one leaf. (#695 @M-Adoo)
- **core**: The padding only reduces the content size and does not affect the border and background size. (#695 @M-Adoo)

## [0.4.0-alpha.24] - 2025-01-22

Expand All @@ -51,7 +51,7 @@ Please only add new entries below the [Unreleased](#unreleased---releasedate) he
- **core**: ensure object safety of `StateReader`, `StateWatcher` and `StateWriter` (#692 @M-Adoo)
- **core**: Support extend custom event. (#684 @wjian23)
- **core**: Added `map_watcher` to `StateWatcher` (#684 @wjian23)
- **core**: Added `ensure_visible` and ScrollableProvider to ScrollableWidget, to support descendant to be showed.(#684 @wjian23)
- **core**: Added `visible_widget` and ScrollableProvider to ScrollableWidget, to support descendant to be showed.(#684 @wjian23)

### Changed

Expand Down
92 changes: 53 additions & 39 deletions core/src/builtin_widgets/scrollable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,53 +96,67 @@ impl ScrollableWidget {
Some(pos - base.to_vector())
}

/// Ensure the given child is visible in the scroll view with the given anchor
pub fn map_to_content(&self, p: Point, child: WidgetId, wnd: &Window) -> Option<Point> {
let content_id = self
.view_id
.as_ref()?
.get()?
.single_child(wnd.tree())?;
let pos = wnd.map_to_global(p, child);
let base = wnd.map_to_global(Point::zero(), content_id);
Some(pos - base.to_vector())
}

/// Keep the content box visible in the scroll view with the given anchor
/// relative to the view.
///
/// If Anchor.x is None, it will anchor the widget to the closest edge of the
/// view in horizontal direction, when the widget is out of the view.
/// If Anchor.y is None, it will anchor the widget to the closest edge of the
/// view in vertical direction, when the widget is out of the view.
pub fn ensure_visible(&mut self, child: WidgetId, anchor: Anchor, wnd: &Window) {
let Some(pos) = self.map_to_view(Point::zero(), child, wnd) else { return };
let Some(size) = wnd.widget_size(child) else { return };
let child_rect = Rect::new(pos, size);
pub fn visible_content_box(&mut self, rect: Rect, anchor: Anchor) {
let view_size = self.scroll_view_size();

let best_auto_position_fn = |min: f32, max: f32, max_limit: f32| {
if (min < 0. && max_limit < max) || (0. < min && max < max_limit) {
min
} else if min.abs() < (max - max_limit).abs() {
0.
} else {
max_limit - (max - min)
}
};
let Anchor { x, y } = anchor;
let top_left = match (x, y) {
(Some(x), Some(y)) => Point::new(
x.into_pixel(child_rect.width(), view_size.width),
y.into_pixel(child_rect.height(), view_size.height),
),
(Some(x), None) => {
let best_y =
best_auto_position_fn(child_rect.min_y(), child_rect.max_y(), view_size.height);
Point::new(x.into_pixel(child_rect.width(), view_size.width), best_y)
}
(None, Some(y)) => {
let best_x = best_auto_position_fn(child_rect.min_x(), child_rect.max_x(), view_size.width);
Point::new(best_x, y.into_pixel(child_rect.height(), view_size.height))
}
(None, None) => {
let best_x = best_auto_position_fn(child_rect.min_x(), child_rect.max_x(), view_size.width);
let best_y =
best_auto_position_fn(child_rect.min_y(), child_rect.max_y(), view_size.height);
Point::new(best_x, best_y)
}
};
let offset_x = anchor
.x
.or_else(|| {
if rect.min_x() > view_size.width {
Some(HAnchor::Right(0.0.into()))
} else if rect.max_x() < 0. {
Some(HAnchor::Left(0.0.into()))
} else {
None
}
})
.map_or(0., |x| rect.min_x() - x.into_pixel(rect.width(), view_size.width));

let offset_y = anchor
.y
.or_else(|| {
if rect.min_y() > view_size.height {
Some(VAnchor::Bottom(0.0.into()))
} else if rect.max_y() < 0. {
Some(VAnchor::Top(0.0.into()))
} else {
None
}
})
.map_or(0., |y| rect.min_y() - y.into_pixel(rect.height(), view_size.height));

self.jump_to(Point::new(offset_x, offset_y));
}

let old = self.get_scroll_pos();
let offset = child_rect.origin - top_left;
self.jump_to(old + offset);
/// Ensure the given child is visible in the scroll view with the given anchor
/// relative to the view.
/// If Anchor.x is None, it will anchor the widget to the closest edge of the
/// view in horizontal direction, when the widget is out of the view.
/// If Anchor.y is None, it will anchor the widget to the closest edge of the
/// view in vertical direction, when the widget is out of the view.
pub fn visible_widget(&mut self, child: WidgetId, anchor: Anchor, wnd: &Window) {
let Some(pos) = self.map_to_content(Point::zero(), child, wnd) else { return };
let Some(size) = wnd.widget_size(child) else { return };
let show_box = Rect::new(pos, size);
self.visible_content_box(show_box, anchor);
}

pub fn scroll(&mut self, x: f32, y: f32) {
Expand Down
10 changes: 10 additions & 0 deletions core/src/declare.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ impl<V: 'static> DeclareInit<V> {
}
}
}

pub fn map<F, U: 'static>(self, f: F) -> DeclareInit<U>
where
F: Fn(V) -> U + 'static,
{
match self {
Self::Value(v) => DeclareInit::Value(f(v)),
Self::Pipe(v) => v.into_pipe().map(f).declare_into(),
}
}
}

impl<T: Default> Default for DeclareInit<T> {
Expand Down
5 changes: 5 additions & 0 deletions core/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,11 @@ impl Window {
self.once_on_lifecycle(f, |msg| matches!(msg, FrameMsg::BeforeLayout(_)))
}

/// Execute the callback when the layout is ready.
pub fn once_layout_ready(&self, f: impl FnOnce() + 'static) {
self.once_on_lifecycle(f, |msg| matches!(msg, FrameMsg::LayoutReady(_)))
}

/// Return an `rxRust` Scheduler, which will guarantee all task add to the
/// scheduler will finished before current frame finished.
#[inline]
Expand Down
4 changes: 0 additions & 4 deletions examples/todos/src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,6 @@ fn input(
}
@ $input {
h_align: HAlign::Stretch,
border: {
let color = Palette::of(BuildCtx::get()).surface_variant().into();
Border::only_bottom(BorderSide { width: 2., color })
},
on_key_down: move |e| {
if e.key_code() == &PhysicalKey::Code(KeyCode::Enter) {
on_submit($input.text().clone());
Expand Down
Binary file modified test_cases/todos/tests/todos_with_default_by_wgpu.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test_cases/todos/tests/todos_with_material_by_wgpu.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
48 changes: 36 additions & 12 deletions themes/material/src/classes/input_cls.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,52 @@
use ribir_core::prelude::*;
use ribir_widgets::input::{TEXT_CARET, TEXT_HIGH_LIGHT};
use ribir_widgets::input::{INPUT, TEXT_CARET, TEXT_SELECTION, TEXTAREA};

use crate::md;

pub(super) fn init(classes: &mut Classes) {
classes.insert(TEXT_CARET, |_w| {
fn_widget! {
let mut w = @ FittedBox {
box_fit: BoxFit::CoverY,
@ { svgs::TEXT_CARET }
};
classes.insert(TEXT_CARET, |w| {
rdl! {
let mut w = FatObj::new(w);
let blink_interval = Duration::from_millis(500);
let u = interval(blink_interval, AppCtx::scheduler())
.subscribe(move |idx| $w.write().opacity = (idx % 2) as f32);

.subscribe(move |idx| $w.write().opacity = (idx % 2) as f32);
let border = BuildCtx::color()
.map(|color| Border::only_left(BorderSide::new(2., color.into())));
@ $w {
clamp: BoxClamp::fixed_width(2.),
border,
on_disposed: move |_| u.unsubscribe()
}
}
.into_widget()
});

classes.insert(TEXT_HIGH_LIGHT, style_class! {
background: Color::from_rgb(181, 215, 254),
radius: md::RADIUS_2,
classes.insert(TEXT_SELECTION, style_class! {
background: {
let color = BuildCtx::color();
color.into_container_color(BuildCtx::get()).map(|c| c.with_alpha(0.8))
}
});

fn input_border(w: Widget) -> Widget {
let mut w = FatObj::new(w);
let blur = Palette::of(BuildCtx::get()).on_surface_variant();
let border = match BuildCtx::color() {
Variant::Stateful(v) => pipe! {
let color = if $w.has_focus() { *$v } else { blur };
Border::all(BorderSide::new(1., color.into()))
}
.declare_into(),
Variant::Value(c) => pipe! {
let color = if $w.has_focus() { c } else { blur };
Border::all(BorderSide::new(1., color.into()))
}
.declare_into(),
};
w.border(border)
.radius(md::RADIUS_2)
.into_widget()
}
classes.insert(INPUT, input_border);
classes.insert(TEXTAREA, input_border);
}
6 changes: 5 additions & 1 deletion themes/material/src/md.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,9 @@ pub const SIZE_36: Size = Size::new(36., 36.);
pub const SIZE_48: Size = Size::new(48., 48.);
pub const SIZE_64: Size = Size::new(64., 64.);

pub const EDGES_2: EdgeInsets = EdgeInsets::all(2.);

pub const EDGES_4: EdgeInsets = EdgeInsets::all(4.);
pub const EDGES_10: EdgeInsets = EdgeInsets::all(10.);
pub const EDGES_HOR_4: EdgeInsets = EdgeInsets::horizontal(4.);
pub const EDGES_VER_4: EdgeInsets = EdgeInsets::vertical(4.);
pub const EDGES_LEFT_4: EdgeInsets = EdgeInsets::only_left(4.);
Expand All @@ -97,6 +98,9 @@ pub const EDGES_BOTTOM_4: EdgeInsets = EdgeInsets::only_bottom(4.);

pub const EDGES_HOR_6: EdgeInsets = EdgeInsets::horizontal(6.);
pub const EDGES_HOR_8: EdgeInsets = EdgeInsets::horizontal(8.);

pub const EDGES_10: EdgeInsets = EdgeInsets::all(10.);

pub const EDGES_HOR_12: EdgeInsets = EdgeInsets::horizontal(12.);

pub const EDGES_16: EdgeInsets = EdgeInsets::all(16.);
Expand Down
Loading

0 comments on commit e87cb7f

Please sign in to comment.