|
@@ -398,14 +398,29 @@ pub async fn create_bottom_button(
|
|
|
window_scale: &PropertyFloat32,
|
|
window_scale: &PropertyFloat32,
|
|
|
i18n_fish: &I18nBabelFish,
|
|
i18n_fish: &I18nBabelFish,
|
|
|
) -> SceneNodePtr {
|
|
) -> SceneNodePtr {
|
|
|
- let node = create_vector_art(&format!("{}_bg", name));
|
|
|
|
|
- let prop = node.get_property("rect").unwrap();
|
|
|
|
|
|
|
+ // Button container: parent-frame rect, button-local children.
|
|
|
|
|
+ let btn_layer = create_layer(&format!("{name}_layer"));
|
|
|
|
|
+ btn_layer.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
|
|
|
|
|
+ let prop = btn_layer.get_property("rect").unwrap();
|
|
|
prop.set_default_f32(0, PADDING_X).unwrap();
|
|
prop.set_default_f32(0, PADDING_X).unwrap();
|
|
|
let code = cc.compile("h - PADDING_X - BUTTON_HEIGHT").unwrap();
|
|
let code = cc.compile("h - PADDING_X - BUTTON_HEIGHT").unwrap();
|
|
|
prop.set_default_expr(1, code).unwrap();
|
|
prop.set_default_expr(1, code).unwrap();
|
|
|
let code = cc.compile("w - PADDING_X * 2.").unwrap();
|
|
let code = cc.compile("w - PADDING_X * 2.").unwrap();
|
|
|
prop.set_default_expr(2, code).unwrap();
|
|
prop.set_default_expr(2, code).unwrap();
|
|
|
prop.set_default_f32(3, BUTTON_HEIGHT).unwrap();
|
|
prop.set_default_f32(3, BUTTON_HEIGHT).unwrap();
|
|
|
|
|
+ btn_layer.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
|
|
|
|
|
+ let btn_layer = btn_layer
|
|
|
|
|
+ .setup(|me| Layer::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
|
|
|
|
|
+ .await;
|
|
|
|
|
+ layer.link(btn_layer.clone());
|
|
|
|
|
+
|
|
|
|
|
+ // Bg outline
|
|
|
|
|
+ let node = create_vector_art(&format!("{}_bg", name));
|
|
|
|
|
+ let prop = node.get_property("rect").unwrap();
|
|
|
|
|
+ prop.set_default_f32(0, 0.).unwrap();
|
|
|
|
|
+ prop.set_default_f32(1, 0.).unwrap();
|
|
|
|
|
+ prop.set_default_expr(2, expr::load_var("w")).unwrap();
|
|
|
|
|
+ prop.set_default_expr(3, expr::load_var("h")).unwrap();
|
|
|
node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
|
|
node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
|
|
|
node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
|
|
node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
|
|
|
let mut shape = VectorShape::new();
|
|
let mut shape = VectorShape::new();
|
|
@@ -420,19 +435,16 @@ pub async fn create_bottom_button(
|
|
|
node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
|
|
node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
|
|
|
let node =
|
|
let node =
|
|
|
node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
|
|
node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
|
|
|
- layer.link(node.clone());
|
|
|
|
|
|
|
+ btn_layer.link(node);
|
|
|
|
|
|
|
|
// Button label text (if provided)
|
|
// Button label text (if provided)
|
|
|
if let Some(text) = label_text {
|
|
if let Some(text) = label_text {
|
|
|
let label_node = create_text(&format!("{}_label", name));
|
|
let label_node = create_text(&format!("{}_label", name));
|
|
|
let prop = label_node.get_property("rect").unwrap();
|
|
let prop = label_node.get_property("rect").unwrap();
|
|
|
- prop.set_default_f32(0, PADDING_X).unwrap();
|
|
|
|
|
- let code = cc
|
|
|
|
|
- .compile("h - PADDING_X - BUTTON_HEIGHT + BUTTON_HEIGHT / 2 - BUTTON_FONTSIZE / 1.8")
|
|
|
|
|
- .unwrap();
|
|
|
|
|
|
|
+ prop.set_default_f32(0, 0.).unwrap();
|
|
|
|
|
+ let code = cc.compile("BUTTON_HEIGHT / 2 - BUTTON_FONTSIZE / 1.8").unwrap();
|
|
|
prop.set_default_expr(1, code).unwrap();
|
|
prop.set_default_expr(1, code).unwrap();
|
|
|
- let code = cc.compile("w - PADDING_X * 2.").unwrap();
|
|
|
|
|
- prop.set_default_expr(2, code).unwrap();
|
|
|
|
|
|
|
+ prop.set_default_expr(2, expr::load_var("w")).unwrap();
|
|
|
prop.set_default_f32(3, BUTTON_HEIGHT).unwrap();
|
|
prop.set_default_f32(3, BUTTON_HEIGHT).unwrap();
|
|
|
label_node.get_property("font_size").unwrap().set_default_f32(0, BUTTON_FONTSIZE).unwrap();
|
|
label_node.get_property("font_size").unwrap().set_default_f32(0, BUTTON_FONTSIZE).unwrap();
|
|
|
label_node.set_property_str(atom, Role::App, "text", text).unwrap();
|
|
label_node.set_property_str(atom, Role::App, "text", text).unwrap();
|
|
@@ -453,76 +465,122 @@ pub async fn create_bottom_button(
|
|
|
)
|
|
)
|
|
|
})
|
|
})
|
|
|
.await;
|
|
.await;
|
|
|
- layer.link(label_node);
|
|
|
|
|
|
|
+ btn_layer.link(label_node);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Button
|
|
// Button
|
|
|
let node = create_button(name);
|
|
let node = create_button(name);
|
|
|
node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
|
|
node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
|
|
|
let prop = node.get_property("rect").unwrap();
|
|
let prop = node.get_property("rect").unwrap();
|
|
|
- prop.set_default_f32(0, PADDING_X).unwrap();
|
|
|
|
|
- let code = cc.compile("h - PADDING_X - BUTTON_HEIGHT").unwrap();
|
|
|
|
|
- prop.set_default_expr(1, code).unwrap();
|
|
|
|
|
- let code = cc.compile("w - PADDING_X * 2.").unwrap();
|
|
|
|
|
- prop.set_default_expr(2, code).unwrap();
|
|
|
|
|
- prop.set_default_f32(3, BUTTON_HEIGHT).unwrap();
|
|
|
|
|
|
|
+ prop.set_default_f32(0, 0.).unwrap();
|
|
|
|
|
+ prop.set_default_f32(1, 0.).unwrap();
|
|
|
|
|
+ prop.set_default_expr(2, expr::load_var("w")).unwrap();
|
|
|
|
|
+ prop.set_default_expr(3, expr::load_var("h")).unwrap();
|
|
|
|
|
|
|
|
let node =
|
|
let node =
|
|
|
node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
|
|
node.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
|
|
|
- layer.link(node.clone());
|
|
|
|
|
|
|
+ btn_layer.link(node.clone());
|
|
|
node
|
|
node
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/// Creates a bottom button with two states: valid (teal) and invalid (grey).
|
|
|
|
|
-/// Returns a tuple of (button_node, bg_valid_node, bg_invalid_node, label_node) for visibility control.
|
|
|
|
|
|
|
+/// Creates a bottom button with two states: valid (teal, active) and
|
|
|
|
|
+/// invalid (grey, inactive). Each state is a self-contained layer, so
|
|
|
|
|
+/// switching state is just an `is_visible` toggle on the two layers.
|
|
|
|
|
+/// Returns (button_node, valid_layer, grey_layer).
|
|
|
pub async fn create_bottom_button_with_states(
|
|
pub async fn create_bottom_button_with_states(
|
|
|
app: &App,
|
|
app: &App,
|
|
|
atom: &mut PropertyAtomicGuard,
|
|
atom: &mut PropertyAtomicGuard,
|
|
|
layer: &SceneNodePtr,
|
|
layer: &SceneNodePtr,
|
|
|
name: &str,
|
|
name: &str,
|
|
|
cc: &mut Compiler,
|
|
cc: &mut Compiler,
|
|
|
- label_text: &str,
|
|
|
|
|
|
|
+ label_valid: &str,
|
|
|
|
|
+ label_grey: &str,
|
|
|
window_scale: &PropertyFloat32,
|
|
window_scale: &PropertyFloat32,
|
|
|
i18n_fish: &I18nBabelFish,
|
|
i18n_fish: &I18nBabelFish,
|
|
|
initial_valid: bool,
|
|
initial_valid: bool,
|
|
|
-) -> (SceneNodePtr, SceneNodePtr, SceneNodePtr, SceneNodePtr) {
|
|
|
|
|
- // Button bg (teal outline - valid state)
|
|
|
|
|
- let bg_valid = create_vector_art(&format!("{}_bg", name));
|
|
|
|
|
- let prop = bg_valid.get_property("rect").unwrap();
|
|
|
|
|
- prop.set_default_f32(0, PADDING_X).unwrap();
|
|
|
|
|
- let code = cc.compile("h - PADDING_X - BUTTON_HEIGHT").unwrap();
|
|
|
|
|
- prop.set_default_expr(1, code).unwrap();
|
|
|
|
|
- let code = cc.compile("w - PADDING_X * 2.").unwrap();
|
|
|
|
|
- prop.set_default_expr(2, code).unwrap();
|
|
|
|
|
- prop.set_default_f32(3, BUTTON_HEIGHT).unwrap();
|
|
|
|
|
- bg_valid.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
|
|
|
|
|
- bg_valid.set_property_bool(atom, Role::App, "is_visible", initial_valid).unwrap();
|
|
|
|
|
- let mut shape = VectorShape::new();
|
|
|
|
|
- shape.add_outline(
|
|
|
|
|
- expr::const_f32(0.),
|
|
|
|
|
- expr::const_f32(0.),
|
|
|
|
|
- expr::load_var("w"),
|
|
|
|
|
- expr::load_var("h"),
|
|
|
|
|
- 1.,
|
|
|
|
|
|
|
+) -> (SceneNodePtr, SceneNodePtr, SceneNodePtr) {
|
|
|
|
|
+ let btn_layer = create_bottom_button_state(
|
|
|
|
|
+ app,
|
|
|
|
|
+ atom,
|
|
|
|
|
+ layer,
|
|
|
|
|
+ name,
|
|
|
|
|
+ cc,
|
|
|
|
|
+ label_valid,
|
|
|
|
|
+ window_scale,
|
|
|
|
|
+ i18n_fish,
|
|
|
COLOR_TEAL,
|
|
COLOR_TEAL,
|
|
|
- );
|
|
|
|
|
- bg_valid.set_property_shape(atom, Role::App, "shape", shape).unwrap();
|
|
|
|
|
- let bg_valid = bg_valid
|
|
|
|
|
- .setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
|
|
|
|
|
- .await;
|
|
|
|
|
- layer.link(bg_valid.clone());
|
|
|
|
|
|
|
+ COLOR_CYAN,
|
|
|
|
|
+ true,
|
|
|
|
|
+ initial_valid,
|
|
|
|
|
+ "",
|
|
|
|
|
+ )
|
|
|
|
|
+ .await;
|
|
|
|
|
+ let grey_layer = create_bottom_button_state(
|
|
|
|
|
+ app,
|
|
|
|
|
+ atom,
|
|
|
|
|
+ layer,
|
|
|
|
|
+ name,
|
|
|
|
|
+ cc,
|
|
|
|
|
+ label_grey,
|
|
|
|
|
+ window_scale,
|
|
|
|
|
+ i18n_fish,
|
|
|
|
|
+ [0.5, 0.5, 0.5, 1.],
|
|
|
|
|
+ [0.5, 0.5, 0.5, 1.],
|
|
|
|
|
+ false,
|
|
|
|
|
+ !initial_valid,
|
|
|
|
|
+ "_grey",
|
|
|
|
|
+ )
|
|
|
|
|
+ .await;
|
|
|
|
|
+
|
|
|
|
|
+ let btn = btn_layer
|
|
|
|
|
+ .lookup_node(&format!("/{name}"))
|
|
|
|
|
+ .unwrap_or_else(|| panic!("bottom button node missing: {name}"));
|
|
|
|
|
+ (btn, btn_layer, grey_layer)
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- // Button bg (grey outline - invalid state)
|
|
|
|
|
- let bg_invalid = create_vector_art(&format!("{}_bg_grey", name));
|
|
|
|
|
- let prop = bg_invalid.get_property("rect").unwrap();
|
|
|
|
|
|
|
+/// One state of a bottom button, wrapped in its own layer so callers can
|
|
|
|
|
+/// switch states by toggling layer visibility. Node names get `suffix`
|
|
|
|
|
+/// appended so both states can live under the same parent.
|
|
|
|
|
+#[allow(clippy::too_many_arguments)]
|
|
|
|
|
+async fn create_bottom_button_state(
|
|
|
|
|
+ app: &App,
|
|
|
|
|
+ atom: &mut PropertyAtomicGuard,
|
|
|
|
|
+ layer: &SceneNodePtr,
|
|
|
|
|
+ name: &str,
|
|
|
|
|
+ cc: &mut Compiler,
|
|
|
|
|
+ label_text: &str,
|
|
|
|
|
+ window_scale: &PropertyFloat32,
|
|
|
|
|
+ i18n_fish: &I18nBabelFish,
|
|
|
|
|
+ outline_color: Color,
|
|
|
|
|
+ label_color: Color,
|
|
|
|
|
+ is_active: bool,
|
|
|
|
|
+ is_visible: bool,
|
|
|
|
|
+ suffix: &str,
|
|
|
|
|
+) -> SceneNodePtr {
|
|
|
|
|
+ // Button container
|
|
|
|
|
+ let btn_layer = create_layer(&format!("{name}_layer{suffix}"));
|
|
|
|
|
+ btn_layer.set_property_bool(atom, Role::App, "is_visible", is_visible).unwrap();
|
|
|
|
|
+ let prop = btn_layer.get_property("rect").unwrap();
|
|
|
prop.set_default_f32(0, PADDING_X).unwrap();
|
|
prop.set_default_f32(0, PADDING_X).unwrap();
|
|
|
let code = cc.compile("h - PADDING_X - BUTTON_HEIGHT").unwrap();
|
|
let code = cc.compile("h - PADDING_X - BUTTON_HEIGHT").unwrap();
|
|
|
prop.set_default_expr(1, code).unwrap();
|
|
prop.set_default_expr(1, code).unwrap();
|
|
|
let code = cc.compile("w - PADDING_X * 2.").unwrap();
|
|
let code = cc.compile("w - PADDING_X * 2.").unwrap();
|
|
|
prop.set_default_expr(2, code).unwrap();
|
|
prop.set_default_expr(2, code).unwrap();
|
|
|
prop.set_default_f32(3, BUTTON_HEIGHT).unwrap();
|
|
prop.set_default_f32(3, BUTTON_HEIGHT).unwrap();
|
|
|
- bg_invalid.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
|
|
|
|
|
- bg_invalid.set_property_bool(atom, Role::App, "is_visible", !initial_valid).unwrap();
|
|
|
|
|
|
|
+ btn_layer.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
|
|
|
|
|
+ let btn_layer = btn_layer
|
|
|
|
|
+ .setup(|me| Layer::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
|
|
|
|
|
+ .await;
|
|
|
|
|
+ layer.link(btn_layer.clone());
|
|
|
|
|
+
|
|
|
|
|
+ // Bg outline
|
|
|
|
|
+ let node = create_vector_art(&format!("{name}_bg{suffix}"));
|
|
|
|
|
+ let prop = node.get_property("rect").unwrap();
|
|
|
|
|
+ prop.set_default_f32(0, 0.).unwrap();
|
|
|
|
|
+ prop.set_default_f32(1, 0.).unwrap();
|
|
|
|
|
+ prop.set_default_expr(2, expr::load_var("w")).unwrap();
|
|
|
|
|
+ prop.set_default_expr(3, expr::load_var("h")).unwrap();
|
|
|
|
|
+ node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
|
|
|
let mut shape = VectorShape::new();
|
|
let mut shape = VectorShape::new();
|
|
|
shape.add_outline(
|
|
shape.add_outline(
|
|
|
expr::const_f32(0.),
|
|
expr::const_f32(0.),
|
|
@@ -530,41 +588,26 @@ pub async fn create_bottom_button_with_states(
|
|
|
expr::load_var("w"),
|
|
expr::load_var("w"),
|
|
|
expr::load_var("h"),
|
|
expr::load_var("h"),
|
|
|
1.,
|
|
1.,
|
|
|
- [0.5, 0.5, 0.5, 1.],
|
|
|
|
|
|
|
+ outline_color,
|
|
|
);
|
|
);
|
|
|
- bg_invalid.set_property_shape(atom, Role::App, "shape", shape).unwrap();
|
|
|
|
|
- let bg_invalid = bg_invalid
|
|
|
|
|
- .setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone()))
|
|
|
|
|
- .await;
|
|
|
|
|
- layer.link(bg_invalid.clone());
|
|
|
|
|
|
|
+ node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
|
|
|
|
|
+ let node =
|
|
|
|
|
+ node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
|
|
|
|
|
+ btn_layer.link(node);
|
|
|
|
|
|
|
|
// Button label text
|
|
// Button label text
|
|
|
- let label_node = create_text(&format!("{}_label", name));
|
|
|
|
|
|
|
+ let label_node = create_text(&format!("{name}_label{suffix}"));
|
|
|
let prop = label_node.get_property("rect").unwrap();
|
|
let prop = label_node.get_property("rect").unwrap();
|
|
|
- prop.set_default_f32(0, PADDING_X).unwrap();
|
|
|
|
|
- let code = cc
|
|
|
|
|
- .compile("h - PADDING_X - BUTTON_HEIGHT + BUTTON_HEIGHT / 2 - BUTTON_FONTSIZE / 1.8")
|
|
|
|
|
- .unwrap();
|
|
|
|
|
- prop.set_default_expr(1, code).unwrap();
|
|
|
|
|
- let code = cc.compile("w - PADDING_X * 2.").unwrap();
|
|
|
|
|
- prop.set_default_expr(2, code).unwrap();
|
|
|
|
|
|
|
+ prop.set_default_f32(0, 0.).unwrap();
|
|
|
|
|
+ prop.set_default_f32(1, BUTTON_HEIGHT / 2. - BUTTON_FONTSIZE / 1.8).unwrap();
|
|
|
|
|
+ prop.set_default_expr(2, expr::load_var("w")).unwrap();
|
|
|
prop.set_default_f32(3, BUTTON_HEIGHT).unwrap();
|
|
prop.set_default_f32(3, BUTTON_HEIGHT).unwrap();
|
|
|
label_node.get_property("font_size").unwrap().set_default_f32(0, BUTTON_FONTSIZE).unwrap();
|
|
label_node.get_property("font_size").unwrap().set_default_f32(0, BUTTON_FONTSIZE).unwrap();
|
|
|
label_node.set_property_str(atom, Role::App, "text", label_text).unwrap();
|
|
label_node.set_property_str(atom, Role::App, "text", label_text).unwrap();
|
|
|
label_node.set_property_enum(atom, Role::App, "text_align", "center").unwrap();
|
|
label_node.set_property_enum(atom, Role::App, "text_align", "center").unwrap();
|
|
|
|
|
|
|
|
let prop = label_node.get_property("text_color").unwrap();
|
|
let prop = label_node.get_property("text_color").unwrap();
|
|
|
- if initial_valid {
|
|
|
|
|
- prop.set_default_f32(0, COLOR_CYAN[0]).unwrap();
|
|
|
|
|
- prop.set_default_f32(1, COLOR_CYAN[1]).unwrap();
|
|
|
|
|
- prop.set_default_f32(2, COLOR_CYAN[2]).unwrap();
|
|
|
|
|
- prop.set_default_f32(3, COLOR_CYAN[3]).unwrap();
|
|
|
|
|
- } else {
|
|
|
|
|
- prop.set_default_f32(0, 0.5).unwrap();
|
|
|
|
|
- prop.set_default_f32(1, 0.5).unwrap();
|
|
|
|
|
- prop.set_default_f32(2, 0.5).unwrap();
|
|
|
|
|
- prop.set_default_f32(3, 1.).unwrap();
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ prop.set_default_f32_multi(&label_color).unwrap();
|
|
|
label_node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
|
|
label_node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
|
|
|
let label_node = label_node
|
|
let label_node = label_node
|
|
|
.setup(|me| {
|
|
.setup(|me| {
|
|
@@ -577,24 +620,22 @@ pub async fn create_bottom_button_with_states(
|
|
|
)
|
|
)
|
|
|
})
|
|
})
|
|
|
.await;
|
|
.await;
|
|
|
- layer.link(label_node.clone());
|
|
|
|
|
|
|
+ btn_layer.link(label_node);
|
|
|
|
|
|
|
|
// Button
|
|
// Button
|
|
|
- let btn = create_button(name);
|
|
|
|
|
- btn.set_property_bool(atom, Role::App, "is_active", true).unwrap();
|
|
|
|
|
|
|
+ let btn = create_button(&format!("{name}{suffix}"));
|
|
|
|
|
+ btn.set_property_bool(atom, Role::App, "is_active", is_active).unwrap();
|
|
|
let prop = btn.get_property("rect").unwrap();
|
|
let prop = btn.get_property("rect").unwrap();
|
|
|
- prop.set_default_f32(0, PADDING_X).unwrap();
|
|
|
|
|
- let code = cc.compile("h - PADDING_X - BUTTON_HEIGHT").unwrap();
|
|
|
|
|
- prop.set_default_expr(1, code).unwrap();
|
|
|
|
|
- let code = cc.compile("w - PADDING_X * 2.").unwrap();
|
|
|
|
|
- prop.set_default_expr(2, code).unwrap();
|
|
|
|
|
- prop.set_default_f32(3, BUTTON_HEIGHT).unwrap();
|
|
|
|
|
|
|
+ prop.set_default_f32(0, 0.).unwrap();
|
|
|
|
|
+ prop.set_default_f32(1, 0.).unwrap();
|
|
|
|
|
+ prop.set_default_expr(2, expr::load_var("w")).unwrap();
|
|
|
|
|
+ prop.set_default_expr(3, expr::load_var("h")).unwrap();
|
|
|
|
|
|
|
|
let btn =
|
|
let btn =
|
|
|
btn.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
|
|
btn.setup(|me| Button::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
|
|
|
- layer.link(btn.clone());
|
|
|
|
|
|
|
+ btn_layer.link(btn);
|
|
|
|
|
|
|
|
- (btn, bg_valid, bg_invalid, label_node)
|
|
|
|
|
|
|
+ btn_layer
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/// Create tooltip component (with auto-hide)
|
|
/// Create tooltip component (with auto-hide)
|