use crate::{
makepad_derive_widget::*,
makepad_draw::*,
widget::*,
};
live_design!{
link widgets;
use link::theme::*;
use makepad_draw::shader::std::*;
use crate::button::Button;
pub DrawFlowBlock = {{DrawFlowBlock}} {}
pub TextFlowBase = {{TextFlow}} {
font_size: 8,
flow: RightWrap,
}
pub TextFlowLinkBase = {{TextFlowLink}} {
link = {
draw_text = {
color: #1a0dab
}
}
}
pub TextFlowLink = <TextFlowLinkBase> {
color: #xa,
color_hover: #xf,
color_down: #x3,
margin:{right:5}
animator: {
hover = {
default: off,
off = {
redraw: true,
from: {all: Forward {duration: 0.01}}
apply: {
hovered: 0.0,
down: 0.0,
}
}
on = {
redraw: true,
from: {
all: Forward {duration: 0.1}
down: Forward {duration: 0.01}
}
apply: {
hovered: [{time: 0.0, value: 1.0}],
down: [{time: 0.0, value: 1.0}],
}
}
down = {
redraw: true,
from: {all: Forward {duration: 0.01}}
apply: {
hovered: [{time: 0.0, value: 1.0}],
down: [{time: 0.0, value: 1.0}],
}
}
}
}
}
pub TextFlow = <TextFlowBase> {
width: Fill, height: Fit,
flow: RightWrap,
width:Fill,
height:Fit,
padding: 0
font_size: (THEME_FONT_SIZE_P),
font_color: (THEME_COLOR_TEXT),
draw_normal: {
text_style: <THEME_FONT_REGULAR> {
font_size: (THEME_FONT_SIZE_P)
}
color: (THEME_COLOR_TEXT)
}
draw_italic: {
text_style: <THEME_FONT_ITALIC> {
font_size: (THEME_FONT_SIZE_P)
}
color: (THEME_COLOR_TEXT)
}
draw_bold: {
text_style: <THEME_FONT_BOLD> {
font_size: (THEME_FONT_SIZE_P)
}
color: (THEME_COLOR_TEXT)
}
draw_bold_italic: {
text_style: <THEME_FONT_BOLD_ITALIC> {
font_size: (THEME_FONT_SIZE_P)
}
color: (THEME_COLOR_TEXT)
}
draw_fixed: {
text_style: <THEME_FONT_CODE> {
font_size: (THEME_FONT_SIZE_P)
}
color: (THEME_COLOR_TEXT)
}
code_layout: {
flow: RightWrap,
padding: <THEME_MSPACE_2> { left: (THEME_SPACE_3), right: (THEME_SPACE_3) }
}
code_walk: { width: Fill, height: Fit }
quote_layout: {
flow: RightWrap,
padding: <THEME_MSPACE_2> { left: (THEME_SPACE_3), right: (THEME_SPACE_3) }
}
quote_walk: { width: Fill, height: Fit, }
list_item_layout: {
flow: RightWrap,
padding: <THEME_MSPACE_1> {}
}
list_item_walk: {
height: Fit, width: Fill,
}
inline_code_padding: <THEME_MSPACE_1> {},
inline_code_margin: <THEME_MSPACE_1> {},
sep_walk: {
width: Fill, height: 4.
margin: <THEME_MSPACE_V_1> {}
}
link = <TextFlowLink> {}
draw_block:{
line_color: (THEME_COLOR_TEXT)
sep_color: (THEME_COLOR_SHADOW)
quote_bg_color: (THEME_COLOR_BG_HIGHLIGHT)
quote_fg_color: (THEME_COLOR_TEXT)
code_color: (THEME_COLOR_BG_HIGHLIGHT)
fn pixel(self) -> vec4 {
let sdf = Sdf2d::viewport(self.pos * self.rect_size);
match self.block_type {
FlowBlockType::Quote => {
sdf.box(
0.,
0.,
self.rect_size.x,
self.rect_size.y,
2.
);
sdf.fill(self.quote_bg_color)
sdf.box(
THEME_SPACE_1,
THEME_SPACE_1,
THEME_SPACE_1,
self.rect_size.y - THEME_SPACE_2,
1.5
);
sdf.fill(self.quote_fg_color);
return sdf.result;
}
FlowBlockType::Sep => {
sdf.box(
0.,
1.,
self.rect_size.x-1,
self.rect_size.y-2.,
2.
);
sdf.fill(self.sep_color);
return sdf.result;
}
FlowBlockType::Code => {
sdf.box(
0.,
0.,
self.rect_size.x,
self.rect_size.y,
2.
);
sdf.fill(self.code_color);
return sdf.result;
}
FlowBlockType::InlineCode => {
sdf.box(
1.,
1.,
self.rect_size.x-2.,
self.rect_size.y-2.,
2.
);
sdf.fill(self.code_color);
return sdf.result;
}
FlowBlockType::Underline => {
sdf.box(
0.,
self.rect_size.y-2,
self.rect_size.x,
2.0,
0.5
);
sdf.fill(self.line_color);
return sdf.result;
}
FlowBlockType::Strikethrough => {
sdf.box(
0.,
self.rect_size.y * 0.45,
self.rect_size.x,
2.0,
0.5
);
sdf.fill(self.line_color);
return sdf.result;
}
}
return #f00
}
}
}
}
#[derive(Live, LiveHook)]
#[live_ignore]
#[repr(u32)]
pub enum FlowBlockType {
#[pick] Quote = shader_enum(1),
Sep = shader_enum(2),
Code = shader_enum(3),
InlineCode = shader_enum(4),
Underline = shader_enum(5),
Strikethrough = shader_enum(6)
}
#[derive(Live, LiveHook, LiveRegister)]
#[repr(C)]
pub struct DrawFlowBlock {
#[deref] draw_super: DrawQuad,
#[live] pub line_color: Vec4,
#[live] pub sep_color: Vec4,
#[live] pub code_color: Vec4,
#[live] pub quote_bg_color: Vec4,
#[live] pub quote_fg_color: Vec4,
#[live] pub block_type: FlowBlockType
}
#[derive(Default)]
pub struct StackCounter(usize);
impl StackCounter{
pub fn push(&mut self){
self.0 += 1;
}
pub fn pop(&mut self){
if self.0 > 0{
self.0 -=1;
}
}
pub fn clear(&mut self){
self.0 = 0
}
pub fn value(&self)->usize{
self.0
}
}
#[derive(Live, Widget)]
pub struct TextFlow {
#[live] pub draw_normal: DrawText,
#[live] pub draw_italic: DrawText,
#[live] pub draw_bold: DrawText,
#[live] pub draw_bold_italic: DrawText,
#[live] pub draw_fixed: DrawText,
#[live] pub draw_block: DrawFlowBlock,
#[live] pub font_size: f32,
#[live] pub font_color: Vec4,
#[walk] walk: Walk,
#[rust] area_stack: SmallVec<[Area;4]>,
#[rust] pub font_sizes: SmallVec<[f32;8]>,
#[rust] pub font_colors: SmallVec<[Vec4;8]>,
#[rust] pub combine_spaces: SmallVec<[bool;4]>,
#[rust] pub ignore_newlines: SmallVec<[bool;4]>,
#[rust] pub bold: StackCounter,
#[rust] pub italic: StackCounter,
#[rust] pub fixed: StackCounter,
#[rust] pub underline: StackCounter,
#[rust] pub strikethrough: StackCounter,
#[rust] pub inline_code: StackCounter,
#[rust] pub item_counter: u64,
#[rust] pub first_thing_on_a_line: bool,
#[rust] pub areas_tracker: RectAreasTracker,
#[layout] layout: Layout,
#[live] quote_layout: Layout,
#[live] quote_walk: Walk,
#[live] code_layout: Layout,
#[live] code_walk: Walk,
#[live] sep_walk: Walk,
#[live] list_item_layout: Layout,
#[live] list_item_walk: Walk,
#[live] pub inline_code_padding: Padding,
#[live] pub inline_code_margin: Margin,
#[live(Margin{top:0.5,bottom:0.5,left:0.0,right:0.0})] pub heading_margin: Margin,
#[live(Margin{top:0.5,bottom:0.5,left:0.0,right:0.0})] pub paragraph_margin: Margin,
#[redraw] #[rust] area:Area,
#[rust] draw_state: DrawStateWrap<DrawState>,
#[rust(Some(Default::default()))] items: Option<ComponentMap<LiveId,(WidgetRef, LiveId)>>,
#[rust] templates: ComponentMap<LiveId, LivePtr>,
}
impl LiveHook for TextFlow{
fn apply_value_instance(&mut self, cx: &mut Cx, apply: &mut Apply, index: usize, nodes: &[LiveNode]) -> usize {
let id = nodes[index].id;
match apply.from {
ApplyFrom::NewFromDoc {file_id} | ApplyFrom::UpdateFromDoc {file_id,..} => {
if nodes[index].origin.has_prop_type(LivePropType::Instance) {
let live_ptr = cx.live_registry.borrow().file_id_index_to_live_ptr(file_id, index);
self.templates.insert(id, live_ptr);
for (node, templ_id) in self.items.as_mut().unwrap().values_mut() {
if *templ_id == id {
node.apply(cx, apply, index, nodes);
}
}
}
else {
cx.apply_error_no_matching_field(live_error_origin!(), index, nodes);
}
}
_ => ()
}
nodes.skip_node(index)
}
}
#[derive(Default)]
pub struct RectAreasTracker{
pub areas: SmallVec<[Area;4]>,
pos: usize,
stack: SmallVec<[usize;2]>,
}
impl RectAreasTracker{
fn clear_stack(&mut self){
self.pos = 0;
self.areas.clear();
self.stack.clear();
}
pub fn push_tracker(&mut self){
self.stack.push(self.pos);
}
pub fn pop_tracker(&mut self)->(usize, usize){
return (self.stack.pop().unwrap(), self.pos)
}
pub fn track_rect(&mut self, cx:&mut Cx2d, rect:Rect){
if self.stack.len() >0{
if self.pos >= self.areas.len(){
self.areas.push(Area::Empty);
}
cx.add_aligned_rect_area(&mut self.areas[self.pos], rect);
self.pos += 1;
}
}
}
#[derive(Clone)]
enum DrawState {
Begin,
Drawing,
}
impl Widget for TextFlow {
fn draw_walk(&mut self, cx: &mut Cx2d, _scope: &mut Scope, walk:Walk)->DrawStep{
if self.draw_state.begin(cx, DrawState::Begin) {
self.begin(cx, walk);
return DrawStep::make_step()
}
if let Some(_) = self.draw_state.get() {
self.end(cx);
self.draw_state.end();
}
DrawStep::done()
}
fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
for (id,(entry,_)) in self.items.as_mut().unwrap().iter_mut(){
scope.with_id(*id, |scope| {
entry.handle_event(cx, event, scope);
});
}
}
}
impl TextFlow{
pub fn begin(&mut self, cx: &mut Cx2d, walk:Walk){
cx.begin_turtle(walk, self.layout);
self.draw_state.set(DrawState::Drawing);
self.draw_block.append_to_draw_call(cx);
self.clear_stacks();
}
fn clear_stacks(&mut self){
self.item_counter = 0;
self.areas_tracker.clear_stack();
self.bold.clear();
self.italic.clear();
self.fixed.clear();
self.underline.clear();
self.strikethrough.clear();
self.inline_code.clear();
self.font_sizes.clear();
self.font_colors.clear();
self.area_stack.clear();
self.combine_spaces.clear();
self.ignore_newlines.clear();
self.first_thing_on_a_line = true;
}
pub fn push_size_rel_scale(&mut self, scale: f64){
self.font_sizes.push(
self.font_sizes.last().unwrap_or(&self.font_size) * (scale as f32)
);
}
pub fn push_size_abs_scale(&mut self, scale: f64){
self.font_sizes.push(
self.font_size * (scale as f32)
);
}
pub fn end(&mut self, cx: &mut Cx2d){
cx.end_turtle_with_area(&mut self.area);
self.items.as_mut().unwrap().retain_visible();
}
pub fn begin_code(&mut self, cx:&mut Cx2d){
self.draw_block.block_type = FlowBlockType::Code;
self.draw_block.begin(cx, self.code_walk, self.code_layout);
self.area_stack.push(self.draw_block.draw_vars.area);
self.first_thing_on_a_line = true;
}
pub fn end_code(&mut self, cx:&mut Cx2d){
self.draw_block.draw_vars.area = self.area_stack.pop().unwrap();
self.draw_block.end(cx);
}
pub fn begin_list_item(&mut self, cx:&mut Cx2d, dot:&str, pad:f64){
let fs = self.font_sizes.last().unwrap_or(&self.font_size);
self.draw_normal.text_style.font_size = *fs as _;
let fc = self.font_colors.last().unwrap_or(&self.font_color);
self.draw_normal.color = *fc;
let pad = self.draw_normal.text_style.font_size as f64 * pad;
cx.begin_turtle(self.list_item_walk, Layout{
padding:Padding{
left: self.list_item_layout.padding.left + pad,
..self.list_item_layout.padding
},
..self.list_item_layout
});
let marker_len = dot.chars().count();
let pos = match marker_len {
1 => {
cx.turtle().pos() - dvec2(pad, 0.0)
},
_ => {
let pad = pad + self.draw_normal.text_style.font_size as f64 * (marker_len - 2) as f64;
cx.turtle().pos() - dvec2(pad, 0.0)
}
};
self.draw_normal.draw_abs(cx, pos, dot);
self.area_stack.push(self.draw_block.draw_vars.area);
}
pub fn end_list_item(&mut self, cx:&mut Cx2d){
cx.end_turtle();
self.first_thing_on_a_line = true;
}
pub fn new_line_collapsed(&mut self, cx:&mut Cx2d){
cx.turtle_new_line();
self.first_thing_on_a_line = true;
}
pub fn new_line_collapsed_with_spacing(&mut self, cx:&mut Cx2d, spacing: f64){
cx.turtle_new_line_with_spacing(spacing);
self.first_thing_on_a_line = true;
}
pub fn sep(&mut self, cx:&mut Cx2d){
self.draw_block.block_type = FlowBlockType::Sep;
self.draw_block.draw_walk(cx, self.sep_walk);
}
pub fn begin_quote(&mut self, cx:&mut Cx2d){
self.draw_block.block_type = FlowBlockType::Quote;
self.draw_block.begin(cx, self.quote_walk, self.quote_layout);
self.area_stack.push(self.draw_block.draw_vars.area);
}
pub fn end_quote(&mut self, cx:&mut Cx2d){
self.draw_block.draw_vars.area = self.area_stack.pop().unwrap();
self.draw_block.end(cx);
}
pub fn draw_item_counted(&mut self, cx: &mut Cx2d, template: LiveId,)->LiveId{
let entry_id = self.new_counted_id();
self.item_with(cx, entry_id, template, |cx, item, tf|{
item.draw_all(cx, &mut Scope::with_data(tf));
});
entry_id
}
pub fn new_counted_id(&mut self)->LiveId{
self.item_counter += 1;
LiveId(self.item_counter)
}
pub fn draw_item(&mut self, cx: &mut Cx2d, entry_id: LiveId, template: LiveId){
self.item_with(cx, entry_id, template, |cx, item, tf|{
item.draw_all(cx, &mut Scope::with_data(tf));
});
}
pub fn draw_item_counted_ref(&mut self, cx: &mut Cx2d, template: LiveId,)->WidgetRef{
let entry_id = self.new_counted_id();
self.item_with(cx, entry_id, template, |cx, item, tf|{
item.draw_all(cx, &mut Scope::with_data(tf));
item.clone()
})
}
pub fn draw_item_ref(&mut self, cx: &mut Cx2d, entry_id: LiveId, template: LiveId)->WidgetRef{
self.item_with(cx, entry_id, template, |cx, item, tf|{
item.draw_all(cx, &mut Scope::with_data(tf));
item.clone()
})
}
pub fn item_with<F,R:Default>(&mut self, cx: &mut Cx2d, entry_id:LiveId, template: LiveId, f:F)->R
where F:FnOnce(&mut Cx2d, &WidgetRef, &mut TextFlow)->R{
let mut items = self.items.take().unwrap();
let r = if let Some(ptr) = self.templates.get(&template) {
let entry = items.get_or_insert(cx, entry_id, | cx | {
(WidgetRef::new_from_ptr(cx, Some(*ptr)), template)
});
f(cx, &entry.0, self)
}else{
R::default()
};
self.items = Some(items);
r
}
pub fn item(&mut self, cx: &mut Cx, entry_id: LiveId, template: LiveId) -> WidgetRef {
if let Some(ptr) = self.templates.get(&template) {
let entry = self.items.as_mut().unwrap().get_or_insert(cx, entry_id, | cx | {
(WidgetRef::new_from_ptr(cx, Some(*ptr)), template)
});
return entry.0.clone()
}
WidgetRef::empty()
}
pub fn item_counted(&mut self, cx: &mut Cx, template: LiveId) -> WidgetRef {
let entry_id = self.new_counted_id();
if let Some(ptr) = self.templates.get(&template) {
let entry = self.items.as_mut().unwrap().get_or_insert(cx, entry_id, | cx | {
(WidgetRef::new_from_ptr(cx, Some(*ptr)), template)
});
return entry.0.clone()
}
WidgetRef::empty()
}
pub fn existing_item(&mut self, entry_id: LiveId) -> WidgetRef {
if let Some(item) = self.items.as_mut().unwrap().get(&entry_id){
item.0.clone()
}
else{
WidgetRef::empty()
}
}
pub fn clear_items(&mut self){
self.items.as_mut().unwrap().clear();
}
pub fn item_with_scope(&mut self, cx: &mut Cx, scope: &mut Scope, entry_id: LiveId, template: LiveId) -> Option<WidgetRef> {
if let Some(ptr) = self.templates.get(&template) {
let entry = self.items.as_mut().unwrap().get_or_insert(cx, entry_id, | cx | {
(WidgetRef::new_from_ptr_with_scope(cx, Some(*ptr), scope), template)
});
return Some(entry.0.clone())
}
None
}
pub fn draw_text(&mut self, cx:&mut Cx2d, text:&str){
if let Some(DrawState::Drawing) = self.draw_state.get(){
if (text == " " || text == "") && self.first_thing_on_a_line{
return
}
let text = if self.first_thing_on_a_line{
text.trim_start().trim_end_matches("\n")
}
else{
text.trim_end_matches("\n")
};
let dt = if self.fixed.value() > 0{
&mut self.draw_fixed
}
else if self.bold.value() > 0{
if self.italic.value() > 0{
&mut self.draw_bold_italic
}
else{
&mut self.draw_bold
}
}
else if self.italic.value() > 0{
&mut self.draw_italic
}
else{
&mut self.draw_normal
};
let font_size = self.font_sizes.last().unwrap_or(&self.font_size);
let font_color = self.font_colors.last().unwrap_or(&self.font_color);
dt.text_style.font_size = *font_size as _;
dt.color = *font_color;
let areas_tracker = &mut self.areas_tracker;
if self.inline_code.value() > 0{
let db = &mut self.draw_block;
db.block_type = FlowBlockType::InlineCode;
if !self.first_thing_on_a_line{
let rect = TextFlow::walk_margin(cx, self.inline_code_margin.left);
areas_tracker.track_rect(cx, rect);
}
dt.draw_walk_resumable_with(cx, text, |cx, mut rect|{
rect.pos -= self.inline_code_padding.left_top();
rect.size += self.inline_code_padding.size();
db.draw_abs(cx, rect);
areas_tracker.track_rect(cx, rect);
});
let rect = TextFlow::walk_margin(cx, self.inline_code_margin.right);
areas_tracker.track_rect(cx, rect);
}
else if self.strikethrough.value() > 0{
let db = &mut self.draw_block;
db.line_color = *font_color;
db.block_type = FlowBlockType::Strikethrough;
dt.draw_walk_resumable_with(cx, text, |cx, rect|{
db.draw_abs(cx, rect);
areas_tracker.track_rect(cx, rect);
});
}
else if self.underline.value() > 0{
let db = &mut self.draw_block;
db.line_color = *font_color;
db.block_type = FlowBlockType::Underline;
dt.draw_walk_resumable_with(cx, text, |cx, rect|{
db.draw_abs(cx, rect);
areas_tracker.track_rect(cx, rect);
});
}
else{
dt.draw_walk_resumable_with(cx, text, |cx, rect|{
areas_tracker.track_rect(cx, rect);
});
}
}
self.first_thing_on_a_line = false;
}
pub fn walk_margin(cx:&mut Cx2d, margin:f64)->Rect{
cx.walk_turtle(Walk{
width: Size::Fixed(margin),
height: Size::Fixed(0.0),
..Default::default()
})
}
pub fn draw_link(&mut self, cx:&mut Cx2d, template:LiveId, data:impl ActionTrait + PartialEq, label:&str){
let entry_id = self.new_counted_id();
self.item_with(cx, entry_id, template, |cx, item, tf|{
item.set_text(cx, label);
item.set_action_data(data);
item.draw_all(cx, &mut Scope::with_data(tf));
})
}
}
#[derive(Debug, Clone, DefaultNone)]
pub enum TextFlowLinkAction {
Clicked {
key_modifiers: KeyModifiers,
},
None,
}
#[derive(Live, Widget)]
struct TextFlowLink {
#[animator] animator: Animator,
#[redraw] #[area] area: Area,
#[live(true)] click_on_down: bool,
#[rust] drawn_areas: SmallVec<[Area; 2]>,
#[live(true)] grab_key_focus: bool,
#[live] margin: Margin,
#[live] hovered: f32,
#[live] down: f32,
#[live] color: Option<Vec4>,
#[live] color_hover: Option<Vec4>,
#[live] color_down: Option<Vec4>,
#[live] pub text: ArcStringMut,
#[action_data] #[rust] action_data: WidgetActionData,
}
impl LiveHook for TextFlowLink {}
impl Widget for TextFlowLink {
fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
if self.animator_handle_event(cx, event).must_redraw() {
if let Some(tf) = scope.data.get_mut::<TextFlow>() {
tf.redraw(cx);
} else {
self.drawn_areas.iter().for_each(|area| area.redraw(cx));
}
}
for area in self.drawn_areas.clone().into_iter() {
match event.hits(cx, area) {
Hit::FingerDown(fe) if fe.is_primary_hit() => {
if self.grab_key_focus {
cx.set_key_focus(self.area());
}
self.animator_play(cx, id!(hover.down));
if self.click_on_down{
cx.widget_action_with_data(
&self.action_data,
self.widget_uid(),
&scope.path,
TextFlowLinkAction::Clicked {
key_modifiers: fe.modifiers,
},
);
}
}
Hit::FingerHoverIn(_) => {
cx.set_cursor(MouseCursor::Hand);
self.animator_play(cx, id!(hover.on));
}
Hit::FingerHoverOut(_) => {
self.animator_play(cx, id!(hover.off));
}
Hit::FingerUp(fe) if fe.is_primary_hit() => {
if fe.is_over {
if !self.click_on_down{
cx.widget_action_with_data(
&self.action_data,
self.widget_uid(),
&scope.path,
TextFlowLinkAction::Clicked {
key_modifiers: fe.modifiers,
},
);
}
if fe.device.has_hovers() {
self.animator_play(cx, id!(hover.on));
} else {
self.animator_play(cx, id!(hover.off));
}
} else {
self.animator_play(cx, id!(hover.off));
}
}
_ => (),
}
}
}
fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, _walk: Walk) -> DrawStep {
let Some(tf) = scope.data.get_mut::<TextFlow>() else {
return DrawStep::done();
};
tf.underline.push();
tf.areas_tracker.push_tracker();
let mut pushed_color = false;
if self.hovered > 0.0 {
if let Some(color) = self.color_hover {
tf.font_colors.push(color);
pushed_color = true;
}
} else if self.down > 0.0 {
if let Some(color) = self.color_down {
tf.font_colors.push(color);
pushed_color = true;
}
} else {
if let Some(color) = self.color {
tf.font_colors.push(color);
pushed_color = true;
}
}
TextFlow::walk_margin(cx, self.margin.left);
tf.draw_text(cx, self.text.as_ref());
TextFlow::walk_margin(cx, self.margin.right);
if pushed_color {
tf.font_colors.pop();
}
tf.underline.pop();
let (start, end) = tf.areas_tracker.pop_tracker();
if self.drawn_areas.len() == end-start{
for i in 0..end-start{
self.drawn_areas[i] = cx.update_area_refs( self.drawn_areas[i],
tf.areas_tracker.areas[i+start]);
}
}
else{
self.drawn_areas = SmallVec::from(
&tf.areas_tracker.areas[start..end]
);
}
DrawStep::done()
}
fn text(&self) -> String {
self.text.as_ref().to_string()
}
fn set_text(&mut self, cx:&mut Cx, v: &str) {
self.text.as_mut_empty().push_str(v);
self.redraw(cx);
}
}