makepad/widgets/src/fold_button.rs
Kevin Boos f307f5b858 Implement the <details>/<summary> widget within Html (#1052)
* draw_text: adopt outer many_instances batch on linux/windows

CodeEditor opens an outer raster batch via DrawText::begin_many_instances
before its glyph loop. The linux/windows branch of DrawText::draw_text
ignored self.many_instances and opened its own nested batch, which
resolved (via find_appendable_drawcall) to the same draw_item whose
`instances` Vec was already swapped out by the outer open, panicking on
unwrap in Cx2d::begin_many_instances.

Mirror what the other platform branch (and draw_rasterized_glyphs_abs)
already do: take self.many_instances on entry, track whether the active
raster batch is the outer one, and hand it back on exit so the caller's
end_many_instances finalizes it. Skip the !drew_raster_this_frame area
clear when the outer batch is still live.

* Html/Markdown fixes: sub/superscript, table outlines/alignment, etc

- **Sub/sup in HTML**: added a `y_shift_scales` stack on `TextFlow`, composed onto `temp_y_shift` in `draw_text`. `<sub>` pushes `+0.55`, `<sup>` pushes `-0.2` in html.rs
- **Sub/sup in Markdown**: now handles `MdEvent::InlineHtml` for `<sub>`/`<sup>` (case-insensitive), using the same stacks.
- **Space after `&amp;` (and other entities)**: the HTML lib's entity decoder now resets `last_non_whitespace` after truncate+push, so the whitespace-collapse check no longer drops the next real space.
- **Table column alignment (Markdown)**: `begin_table_cell` takes `align_x: f64`; tracks `Tag::Table` alignments and a per-row column index, passing each cell's `Alignment` through.
- **Table column alignment (HTML)**: `<td>`/`<th>` now honor `align="…"` and inline `style="text-align: …"` via new `cell_align_x` / `align_keyword_to_x` helpers in html.rs.
- **Per-row text alignment plumbing**: new `layout_align` field on `DrawText` is passed to the layouter, which already supports per-row alignment. `TextFlow` propagates a `cell_text_align_x` into it. This is the actual fix that makes cell alignment visible.
- **Wrap-flow alignment scaffolding**: implemented the previously-stubbed `Flow::Right { wrap: true }` branch in the turtle logic. Useful for non-text wrapping walks; text goes through the layouter path above.
- Add examples to uizoo: three new tables in both markdown and html tab -- a plain one, a left/center/right aligned one, and a numeric all-right-aligned one. They cover bold/italic/code/links/sub-sup/emoji/entities/strikethrough inside cells.

* minor cleanup; prefer `style` over `align` HTML tag

* Implement the `<details>`/`summary` widget within Html

* cleanup/improvement

* spacing and size consistency for details/summary header
2026-04-19 10:13:49 +02:00

295 lines
8.8 KiB
Rust

use crate::{
animator::{Animate, Animator, AnimatorAction, AnimatorImpl, Play},
makepad_derive_widget::*,
makepad_draw::*,
widget::*,
};
script_mod! {
use mod.prelude.widgets_internal.*
use mod.widgets.*
mod.widgets.FoldButtonBase = #(FoldButton::register_widget(vm))
mod.widgets.FoldButton = set_type_default() do mod.widgets.FoldButtonBase{
height: 20
width: 15
margin: Inset{left: 0.}
draw_bg +: {
active: instance(1.0) // Default to open state (matches animator default: @on)
hover: instance(0.0)
color: uniform(theme.color_label_inner)
color_hover: uniform(theme.color_label_inner_hover)
color_active: uniform(theme.color_label_inner_active)
fade: uniform(1.0)
pixel: fn() {
let sz = 2.5
let c = vec2(5.0, self.rect_size.y * 0.4)
let sdf = Sdf2d.viewport(self.pos * self.rect_size)
sdf.clear(vec4(0.))
// we have 3 points, and need to rotate around its center
sdf.rotate(self.active * 0.5 * PI + 0.5 * PI, c.x, c.y)
sdf.move_to(c.x - sz, c.y + sz)
sdf.line_to(c.x, c.y - sz)
sdf.line_to(c.x + sz, c.y + sz)
sdf.close_path()
sdf.fill(
mix(
mix(self.color, self.color_hover, self.hover)
mix(self.color_active, self.color_hover, self.hover)
self.active
)
)
return sdf.result * self.fade
}
}
animator: Animator{
hover: {
default: @off
off: AnimatorState{
from: {all: Forward {duration: 0.1}}
apply: {draw_bg: {hover: 0.0}}
}
on: AnimatorState{
from: {all: Snap}
apply: {draw_bg: {hover: 1.0}}
}
}
active: {
default: @on
off: AnimatorState{
from: {all: Forward {duration: 0.2}}
ease: ExpDecay {d1: 0.96, d2: 0.97}
redraw: true
apply: {
active: 0.0
draw_bg: {active: 0.0}
}
}
on: AnimatorState{
from: {all: Forward {duration: 0.2}}
ease: ExpDecay {d1: 0.98, d2: 0.95}
redraw: true
apply: {
active: 1.0
draw_bg: {active: 1.0}
}
}
}
}
}
}
#[derive(Clone, Debug, Default)]
pub enum FoldButtonAction {
#[default]
None,
Opening,
Closing,
Animating(f64),
}
#[derive(Script, ScriptHook, Widget, Animator)]
pub struct FoldButton {
#[uid]
uid: WidgetUid,
#[source]
source: ScriptObjectRef,
#[apply_default]
animator: Animator,
#[redraw]
#[live]
draw_bg: DrawQuad,
#[live]
abs_size: DVec2,
#[live]
abs_offset: DVec2,
#[walk]
walk: Walk,
#[live]
active: f64,
#[action_data]
#[rust]
action_data: WidgetActionData,
}
impl FoldButton {
pub fn set_is_open(&mut self, cx: &mut Cx, is_open: bool, animate: Animate) {
self.animator_toggle(cx, is_open, animate, ids!(active.on), ids!(active.off))
}
pub fn is_open(&self, cx: &Cx) -> bool {
self.animator_in_state(cx, ids!(active.on))
}
/// Overrides the triangle's base, hover, and active colors with a single
/// value. Intended for callers that want the triangle to match the color
/// of surrounding content (e.g. the HTML `<summary>` text color).
pub fn set_draw_color(&mut self, cx: &Cx, color: Vec4f) {
let values = [color.x, color.y, color.z, color.w];
self.draw_bg
.draw_vars
.set_uniform(cx, live_id!(color), &values);
self.draw_bg
.draw_vars
.set_uniform(cx, live_id!(color_hover), &values);
self.draw_bg
.draw_vars
.set_uniform(cx, live_id!(color_active), &values);
}
pub fn draw_walk_fold_button(&mut self, cx: &mut Cx2d, walk: Walk) {
self.draw_bg.draw_walk(cx, walk);
}
pub fn area(&self) -> Area {
self.draw_bg.area()
}
pub fn draw_abs(&mut self, cx: &mut Cx2d, pos: DVec2) {
self.draw_bg.draw_abs(
cx,
Rect {
pos: pos + self.abs_offset,
size: self.abs_size,
},
);
}
pub fn opening(&self, actions: &Actions) -> bool {
if let Some(item) = actions.find_widget_action(self.widget_uid()) {
if let FoldButtonAction::Opening = item.cast() {
return true;
}
}
false
}
pub fn closing(&self, actions: &Actions) -> bool {
if let Some(item) = actions.find_widget_action(self.widget_uid()) {
if let FoldButtonAction::Closing = item.cast() {
return true;
}
}
false
}
pub fn animating(&self, actions: &Actions) -> Option<f64> {
if let Some(item) = actions.find_widget_action(self.widget_uid()) {
if let FoldButtonAction::Animating(v) = item.cast() {
return Some(v);
}
}
None
}
}
impl Widget for FoldButton {
fn handle_event(&mut self, cx: &mut Cx, event: &Event, _scope: &mut Scope) {
let uid = self.widget_uid();
let res = self.animator_handle_event(cx, event);
if res.must_redraw() {
// Use self.active which is updated by the animator's script_apply
// (draw_bg.get_instance returns the OLD value from the previous draw)
cx.widget_action_with_data(
&self.action_data,
uid,
FoldButtonAction::Animating(self.active),
);
self.draw_bg.redraw(cx);
}
match event.hits(cx, self.draw_bg.area()) {
Hit::FingerDown(_fe) => {
if self.animator_in_state(cx, ids!(active.on)) {
self.animator_play(cx, ids!(active.off));
cx.widget_action_with_data(&self.action_data, uid, FoldButtonAction::Closing)
} else {
self.animator_play(cx, ids!(active.on));
cx.widget_action_with_data(&self.action_data, uid, FoldButtonAction::Opening)
}
self.animator_play(cx, ids!(hover.on));
}
Hit::FingerHoverIn(_) => {
cx.set_cursor(MouseCursor::Hand);
self.animator_play(cx, ids!(hover.on));
}
Hit::FingerHoverOut(_) => {
self.animator_play(cx, ids!(hover.off));
}
Hit::FingerUp(fe) => {
if fe.is_over {
if fe.device.has_hovers() {
self.animator_play(cx, ids!(hover.on));
} else {
self.animator_play(cx, ids!(hover.off));
}
} else {
self.animator_play(cx, ids!(hover.off));
}
}
_ => (),
};
}
fn draw_walk(&mut self, cx: &mut Cx2d, _scope: &mut Scope, walk: Walk) -> DrawStep {
self.draw_walk_fold_button(cx, walk);
DrawStep::done()
}
}
impl FoldButtonRef {
pub fn opening(&self, actions: &Actions) -> bool {
if let Some(item) = actions.find_widget_action(self.widget_uid()) {
if let FoldButtonAction::Opening = item.cast() {
return true;
}
}
false
}
pub fn closing(&self, actions: &Actions) -> bool {
if let Some(item) = actions.find_widget_action(self.widget_uid()) {
if let FoldButtonAction::Closing = item.cast() {
return true;
}
}
false
}
pub fn animating(&self, actions: &Actions) -> Option<f64> {
if let Some(item) = actions.find_widget_action(self.widget_uid()) {
if let FoldButtonAction::Animating(v) = item.cast() {
return Some(v);
}
}
None
}
pub fn open_float(&self) -> f64 {
if let Some(inner) = self.borrow() {
inner.active
} else {
1.0
}
}
pub fn set_is_open(&self, cx: &mut Cx, is_open: bool, animate: Animate) {
if let Some(mut inner) = self.borrow_mut() {
inner.set_is_open(cx, is_open, animate);
}
}
pub fn is_open(&self, cx: &Cx) -> bool {
self.borrow().map_or(true, |inner| inner.is_open(cx))
}
}