diff --git a/emigui/src/containers/area.rs b/emigui/src/containers/area.rs index b61beac7..64e4b5a6 100644 --- a/emigui/src/containers/area.rs +++ b/emigui/src/containers/area.rs @@ -163,7 +163,8 @@ impl Prepared { } else { None }; - let move_interact = ctx.interact(layer, clip_rect, rect, interact_id); + let move_interact = + ctx.interact(layer, clip_rect, rect, interact_id, Sense::click_and_drag()); let input = ctx.input(); if move_interact.active { diff --git a/emigui/src/containers/collapsing_header.rs b/emigui/src/containers/collapsing_header.rs index c756c9a6..aa7cc678 100644 --- a/emigui/src/containers/collapsing_header.rs +++ b/emigui/src/containers/collapsing_header.rs @@ -96,7 +96,7 @@ impl State { &mut self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R, - ) -> Option<(R, InteractInfo)> { + ) -> Option<(R, Rect)> { let openness = self.openness(ui); let animate = 0.0 < openness && openness < 1.0; if animate { @@ -132,7 +132,7 @@ impl State { })) } else if self.open { let r_interact = ui.add_custom(add_contents); - let full_size = r_interact.1.rect.size(); + let full_size = r_interact.1.size(); self.open_height = Some(full_size.y); Some(r_interact) } else { @@ -190,13 +190,13 @@ impl CollapsingHeader { let desired_width = text_max_x - available.left(); let desired_width = desired_width.max(available.width()); - let interact = ui.reserve_space( - vec2( - desired_width, - galley.size.y + 2.0 * ui.style().button_padding.y, - ), - Some(id), + let size = vec2( + desired_width, + galley.size.y + 2.0 * ui.style().button_padding.y, ); + + let rect = ui.allocate_space(size); + let interact = ui.interact(rect, id, Sense::click()); let text_pos = pos2(text_pos.x, interact.rect.center().y - galley.size.y / 2.0); let mut state = State::from_memory_with_default_open(ui, id, default_open); diff --git a/emigui/src/containers/menu.rs b/emigui/src/containers/menu.rs index 32d8c601..62887172 100644 --- a/emigui/src/containers/menu.rs +++ b/emigui/src/containers/menu.rs @@ -20,7 +20,7 @@ impl Default for BarState { } } -pub fn bar(ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> (R, InteractInfo) { +pub fn bar(ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> (R, Rect) { ui.inner_layout(Layout::horizontal(Align::Center), |ui| { Frame::menu_bar(ui.style()).show(ui, |ui| { let mut style = ui.style().clone(); diff --git a/emigui/src/containers/resize.rs b/emigui/src/containers/resize.rs index fd805162..ba8ec9c4 100644 --- a/emigui/src/containers/resize.rs +++ b/emigui/src/containers/resize.rs @@ -209,7 +209,7 @@ impl Resize { position + state.size + self.handle_offset - corner_size, corner_size, ); - let corner_interact = ui.interact_rect(corner_rect, id.with("corner")); + let corner_interact = ui.interact(corner_rect, id.with("corner"), Sense::drag()); if corner_interact.active { if let Some(mouse_pos) = ui.input().mouse.pos { @@ -304,7 +304,7 @@ impl Resize { // state.size = state.size.clamp(self.min_size..=self.max_size); state.size = state.size.round(); // TODO: round to pixels - ui.reserve_space(state.size, None); + ui.allocate_space(state.size); // ------------------------------ diff --git a/emigui/src/containers/scroll_area.rs b/emigui/src/containers/scroll_area.rs index 63cc6346..d26884f0 100644 --- a/emigui/src/containers/scroll_area.rs +++ b/emigui/src/containers/scroll_area.rs @@ -146,8 +146,8 @@ impl Prepared { let content_is_too_small = content_size.y > inner_rect.height(); if content_is_too_small { - // Dragg contents to scroll (for touch screens mostly): - let content_interact = ui.interact_rect(inner_rect, id.with("area")); + // Drag contents to scroll (for touch screens mostly): + let content_interact = ui.interact(inner_rect, id.with("area"), Sense::drag()); if content_interact.active { state.offset.y -= ui.input().mouse.delta.y; } @@ -181,7 +181,7 @@ impl Prepared { // intentionally use same id for inside and outside of handle let interact_id = id.with("vertical"); - let handle_interact = ui.interact_rect(handle_rect, interact_id); + let handle_interact = ui.interact(handle_rect, interact_id, Sense::click_and_drag()); if let Some(mouse_pos) = ui.input().mouse.pos { if handle_interact.active { @@ -191,7 +191,8 @@ impl Prepared { } } else { // Check for mouse down outside handle: - let scroll_bg_interact = ui.interact_rect(outer_scroll_rect, interact_id); + let scroll_bg_interact = + ui.interact(outer_scroll_rect, interact_id, Sense::click_and_drag()); if scroll_bg_interact.active { // Center scroll at mouse pos: @@ -235,7 +236,7 @@ impl Prepared { // content_size.y.min(inner_rect.size().y), // respect vertical height. // ); let size = outer_rect.size(); - ui.reserve_space(size, None); + ui.allocate_space(size); state.offset.y = state.offset.y.min(content_size.y - inner_rect.height()); state.offset.y = state.offset.y.max(0.0); diff --git a/emigui/src/containers/window.rs b/emigui/src/containers/window.rs index 098b6f8a..8977d49e 100644 --- a/emigui/src/containers/window.rs +++ b/emigui/src/containers/window.rs @@ -529,10 +529,10 @@ fn show_title_bar( { // TODO: make clickable radius larger - ui.reserve_space(vec2(0.0, 0.0), None); // HACK: will add left spacing + ui.allocate_space(vec2(0.0, 0.0)); // HACK: will add left spacing - let collapse_button_interact = - ui.reserve_space(Vec2::splat(button_size), Some(collapsing_id)); + let rect = ui.allocate_space(Vec2::splat(button_size)); + let collapse_button_interact = ui.interact(rect, collapsing_id, Sense::click()); if collapse_button_interact.clicked { // TODO: also do this when double-clicking window title collapsing.toggle(ui); @@ -541,7 +541,7 @@ fn show_title_bar( } let title_galley = title_label.layout(ui); - let title_rect = ui.reserve_space(title_galley.size, None).rect; + let title_rect = ui.allocate_space(title_galley.size); if show_close_button { // Reserve space for close button which will be added later: @@ -566,7 +566,7 @@ fn show_title_bar( }); TitleBar { - rect: tb_interact.1.rect, + rect: tb_interact.1, ..tb_interact.0 } } @@ -577,8 +577,8 @@ impl TitleBar { .paint_galley(ui, self.title_rect.min, self.title_galley); } - pub fn close_button_ui(&self, ui: &mut Ui, content: &Option) -> InteractInfo { - let right = content.map(|c| c.rect.right()).unwrap_or(self.rect.right()); + pub fn close_button_ui(&self, ui: &mut Ui, content: &Option) -> InteractInfo { + let right = content.map(|c| c.right()).unwrap_or(self.rect.right()); let button_size = ui.style().start_icon_width; let button_rect = Rect::from_min_size( @@ -595,7 +595,7 @@ impl TitleBar { fn close_button(ui: &mut Ui, rect: Rect) -> InteractInfo { let close_id = ui.make_child_id("window_close_button"); - let interact = ui.interact_rect(rect, close_id); + let interact = ui.interact(rect, close_id, Sense::click()); ui.expand_to_include_child(interact.rect); let stroke_color = ui.style().interact(&interact).stroke_color; diff --git a/emigui/src/context.rs b/emigui/src/context.rs index 69de44b7..f7c0d587 100644 --- a/emigui/src/context.rs +++ b/emigui/src/context.rs @@ -148,16 +148,19 @@ impl Context { fn begin_frame_mut(&mut self, new_raw_input: RawInput) { if !self.input.mouse.down || self.input.mouse.pos.is_none() { + // mouse was not down last frame self.memory().active_id = None; let window_interaction = self.memory().window_interaction.take(); if let Some(window_interaction) = window_interaction { - let area_layer = window_interaction.area_layer; - let area_state = self.memory().areas.get(area_layer.id).clone(); - if let Some(mut area_state) = area_state { + if !window_interaction.is_resize() { // Throw windows because it is fun: - area_state.vel = self.input().mouse.velocity; - self.memory().areas.set_state(area_layer, area_state); + let area_layer = window_interaction.area_layer; + let area_state = self.memory().areas.get(area_layer.id).clone(); + if let Some(mut area_state) = area_state { + area_state.vel = self.input().mouse.velocity; + self.memory().areas.set_state(area_layer, area_state); + } } } } @@ -287,15 +290,38 @@ impl Context { clip_rect: Rect, rect: Rect, interaction_id: Option, + sense: Sense, ) -> InteractInfo { let interact_rect = rect.expand2(0.5 * self.style().item_spacing); // make it easier to click. TODO: nice way to do this let hovered = self.contains_mouse(layer, clip_rect, interact_rect); + if interaction_id.is_none() || sense == Sense::nothing() { + // Not interested in input: + return InteractInfo { + rect, + hovered, + clicked: false, + active: false, + }; + } + let interaction_id = interaction_id.unwrap(); + let mut memory = self.memory(); - let active = interaction_id.is_some() && memory.active_id == interaction_id; + let active = memory.active_id == Some(interaction_id); + + if active && !sense.drag && !self.input().mouse.could_be_click { + // Aborted click + memory.active_id = None; + return InteractInfo { + rect, + hovered: false, + clicked: false, + active: false, + }; + } if self.input.mouse.pressed { - if hovered && interaction_id.is_some() { + if hovered { if memory.active_id.is_some() { // Already clicked something else this frame InteractInfo { @@ -305,7 +331,8 @@ impl Context { active: false, } } else { - memory.active_id = interaction_id; + // start of a click or drag + memory.active_id = Some(interaction_id); InteractInfo { rect, hovered, @@ -314,6 +341,7 @@ impl Context { } } } else { + // miss InteractInfo { rect, hovered, @@ -325,7 +353,7 @@ impl Context { InteractInfo { rect, hovered, - clicked: hovered && active && self.input().mouse.could_be_click, + clicked: hovered && active, active, } } else if self.input.mouse.down { diff --git a/emigui/src/examples/app.rs b/emigui/src/examples/app.rs index 8b3dff6d..b2c74cd5 100644 --- a/emigui/src/examples/app.rs +++ b/emigui/src/examples/app.rs @@ -394,11 +394,7 @@ impl BoxPainting { ui.add(Slider::usize(&mut self.num_boxes, 0..=5).text("num_boxes")); let pos = ui - .reserve_space( - vec2(self.size.x * (self.num_boxes as f32), self.size.y), - None, - ) - .rect + .allocate_space(vec2(self.size.x * (self.num_boxes as f32), self.size.y)) .min; let mut cmds = vec![]; @@ -438,7 +434,8 @@ impl Painting { } fn content(&mut self, ui: &mut Ui) { - let interact = ui.reserve_space(ui.available_finite().size(), Some(ui.id())); + let rect = ui.allocate_space(ui.available_finite().size()); + let interact = ui.interact(rect, ui.id(), Sense::drag()); let rect = interact.rect; ui.set_clip_rect(ui.clip_rect().intersect(rect)); // Make sure we don't paint out of bounds diff --git a/emigui/src/introspection.rs b/emigui/src/introspection.rs index ac3149f9..2a7695db 100644 --- a/emigui/src/introspection.rs +++ b/emigui/src/introspection.rs @@ -17,8 +17,7 @@ impl Texture { if size.x > ui.available().width() { size *= ui.available().width() / size.x; } - let interact = ui.reserve_space(size, None); - let rect = interact.rect; + let rect = ui.allocate_space(size); let top_left = Vertex { pos: rect.min, uv: (0, 0), @@ -33,10 +32,10 @@ impl Texture { triangles.add_rect(top_left, bottom_right); ui.add_paint_cmd(PaintCmd::Triangles(triangles)); - if interact.hovered { + if ui.hovered(rect) { show_tooltip(ui.ctx(), |ui| { let pos = ui.top_left(); - let zoom_rect = ui.reserve_space(vec2(128.0, 128.0), None).rect; + let zoom_rect = ui.allocate_space(vec2(128.0, 128.0)); let u = remap_clamp(pos.x, rect.range_x(), 0.0..=self.width as f32 - 1.0).round(); let v = remap_clamp(pos.y, rect.range_y(), 0.0..=self.height as f32 - 1.0).round(); diff --git a/emigui/src/types.rs b/emigui/src/types.rs index 7316bf57..a7ec6222 100644 --- a/emigui/src/types.rs +++ b/emigui/src/types.rs @@ -111,3 +111,44 @@ impl GuiResponse { } // ---------------------------------------------------------------------------- + +/// What sort of interaction is a widget sensitive to? +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Sense { + /// buttons, sliders, windows ... + pub click: bool, + + /// sliders, windows, scroll bars, scroll areas ... + pub drag: bool, +} + +impl Sense { + pub fn nothing() -> Self { + Self { + click: false, + drag: false, + } + } + + pub fn click() -> Self { + Self { + click: true, + drag: false, + } + } + + pub fn drag() -> Self { + Self { + click: false, + drag: true, + } + } + + /// e.g. a slider or window + pub fn click_and_drag() -> Self { + Self { + click: true, + drag: true, + } + } +} diff --git a/emigui/src/ui.rs b/emigui/src/ui.rs index ccdf2339..02b4bf88 100644 --- a/emigui/src/ui.rs +++ b/emigui/src/ui.rs @@ -303,14 +303,18 @@ impl Ui { // ------------------------------------------------------------------------ // Interaction - /// Check for clicks on this entire ui (rect()) - pub fn interact_whole(&self) -> InteractInfo { - self.interact_rect(self.rect(), self.id) + pub fn interact(&self, rect: Rect, id: Id, sense: Sense) -> InteractInfo { + self.ctx + .interact(self.layer, self.clip_rect, rect, Some(id), sense) } - pub fn interact_rect(&self, rect: Rect, id: Id) -> InteractInfo { + pub fn interact_hover(&self, rect: Rect) -> InteractInfo { self.ctx - .interact(self.layer, self.clip_rect, rect, Some(id)) + .interact(self.layer, self.clip_rect, rect, None, Sense::nothing()) + } + + pub fn hovered(&self, rect: Rect) -> bool { + self.interact_hover(rect).hovered } #[must_use] @@ -340,15 +344,6 @@ impl Ui { /// for `Justified` aligned layouts, like in menus. /// /// You may get LESS space than you asked for if the current layout won't fit what you asked for. - /// - /// TODO: remove, or redesign or something and start using allocate_space - pub fn reserve_space(&mut self, child_size: Vec2, interaction_id: Option) -> InteractInfo { - let rect = self.allocate_space(child_size); - - self.ctx - .interact(self.layer, self.clip_rect, rect, interaction_id) - } - pub fn allocate_space(&mut self, child_size: Vec2) -> Rect { let child_size = self.round_vec_to_pixels(child_size); self.cursor = self.round_pos_to_pixels(self.cursor); @@ -530,25 +525,21 @@ impl Ui { /// Just because you ask for a lot of space does not mean you have to use it! /// After `add_contents` is called the contents of `bounding_size` /// will decide how much space will be used in the parent ui. - pub fn add_custom_contents( - &mut self, - size: Vec2, - add_contents: impl FnOnce(&mut Ui), - ) -> InteractInfo { + pub fn add_custom_contents(&mut self, size: Vec2, add_contents: impl FnOnce(&mut Ui)) -> Rect { let size = size.min(self.available().size()); let child_rect = Rect::from_min_size(self.cursor, size); let mut child_ui = self.child_ui(child_rect); add_contents(&mut child_ui); - self.reserve_space(child_ui.bounding_size(), None) + self.allocate_space(child_ui.bounding_size()) } /// Create a child ui - pub fn add_custom(&mut self, add_contents: impl FnOnce(&mut Ui) -> R) -> (R, InteractInfo) { + pub fn add_custom(&mut self, add_contents: impl FnOnce(&mut Ui) -> R) -> (R, Rect) { let child_rect = self.available(); let mut child_ui = self.child_ui(child_rect); let r = add_contents(&mut child_ui); let size = child_ui.bounding_size(); - (r, self.reserve_space(size, None)) + (r, self.allocate_space(size)) } /// Create a child ui which is indented to the right @@ -556,7 +547,7 @@ impl Ui { &mut self, id_source: impl Hash, add_contents: impl FnOnce(&mut Ui) -> R, - ) -> (R, InteractInfo) { + ) -> (R, Rect) { assert!( self.layout().dir() == Direction::Vertical, "You can only indent vertical layouts" @@ -580,7 +571,7 @@ impl Ui { self.style.line_width, )); - (ret, self.reserve_space(indent + size, None)) + (ret, self.allocate_space(indent + size)) } pub fn left_column(&mut self, width: f32) -> Ui { @@ -609,12 +600,12 @@ impl Ui { } /// Start a ui with horizontal layout - pub fn horizontal(&mut self, add_contents: impl FnOnce(&mut Ui) -> R) -> (R, InteractInfo) { + pub fn horizontal(&mut self, add_contents: impl FnOnce(&mut Ui) -> R) -> (R, Rect) { self.inner_layout(Layout::horizontal(Align::Min), add_contents) } /// Start a ui with vertical layout - pub fn vertical(&mut self, add_contents: impl FnOnce(&mut Ui) -> R) -> (R, InteractInfo) { + pub fn vertical(&mut self, add_contents: impl FnOnce(&mut Ui) -> R) -> (R, Rect) { self.inner_layout(Layout::vertical(Align::Min), add_contents) } @@ -622,7 +613,7 @@ impl Ui { &mut self, layout: Layout, add_contents: impl FnOnce(&mut Self) -> R, - ) -> (R, InteractInfo) { + ) -> (R, Rect) { let child_rect = Rect::from_min_max(self.cursor, self.bottom_right()); let mut child_ui = Self { ..self.child_ui(child_rect) @@ -630,8 +621,8 @@ impl Ui { child_ui.set_layout(layout); // HACK: need a separate call right now let ret = add_contents(&mut child_ui); let size = child_ui.bounding_size(); - let interact = self.reserve_space(size, None); - (ret, interact) + let rect = self.allocate_space(size); + (ret, rect) } /// Temporarily split split an Ui into several columns. @@ -678,7 +669,7 @@ impl Ui { } let size = vec2(self.available().width().max(sum_width), max_height); - self.reserve_space(size, None); + self.allocate_space(size); result } diff --git a/emigui/src/widgets.rs b/emigui/src/widgets.rs index 6c5bcbd9..23e9e098 100644 --- a/emigui/src/widgets.rs +++ b/emigui/src/widgets.rs @@ -108,9 +108,9 @@ macro_rules! label { impl Widget for Label { fn ui(self, ui: &mut Ui) -> GuiResponse { let galley = self.layout(ui); - let interact = ui.reserve_space(galley.size, None); - self.paint_galley(ui, interact.rect.min, galley); - ui.response(interact) + let rect = ui.allocate_space(galley.size); + self.paint_galley(ui, rect.min, galley); + ui.response(ui.interact_hover(rect)) } } @@ -158,7 +158,8 @@ impl Widget for Hyperlink { let id = ui.make_child_id(&url); let font = &ui.fonts()[text_style]; let galley = font.layout_multiline(text, ui.available().width()); - let interact = ui.reserve_space(galley.size, Some(id)); + let rect = ui.allocate_space(galley.size); + let interact = ui.interact(rect, id, Sense::click()); if interact.hovered { ui.ctx().output().cursor_icon = CursorIcon::PointingHand; } @@ -239,7 +240,8 @@ impl Widget for Button { let padding = ui.style().button_padding; let mut size = galley.size + 2.0 * padding; size.y = size.y.max(ui.style().clickable_diameter); - let interact = ui.reserve_space(size, Some(id)); + let rect = ui.allocate_space(size); + let interact = ui.interact(rect, id, Sense::click()); let text_cursor = interact.rect.left_center() + vec2(padding.x, -0.5 * galley.size.y); let bg_fill_color = fill_color.or(ui.style().interact(&interact).bg_fill_color); ui.add_paint_cmd(PaintCmd::Rect { @@ -291,13 +293,12 @@ impl<'a> Widget for Checkbox<'a> { let text_style = TextStyle::Button; let font = &ui.fonts()[text_style]; let galley = font.layout_single_line(text); - let interact = ui.reserve_space( - ui.style().button_padding - + vec2(ui.style().start_icon_width, 0.0) - + galley.size - + ui.style().button_padding, - Some(id), - ); + let size = ui.style().button_padding + + vec2(ui.style().start_icon_width, 0.0) + + galley.size + + ui.style().button_padding; + let rect = ui.allocate_space(size); + let interact = ui.interact(rect, id, Sense::click()); let text_cursor = interact.rect.min + ui.style().button_padding + vec2(ui.style().start_icon_width, 0.0); if interact.clicked { @@ -370,13 +371,12 @@ impl Widget for RadioButton { let text_style = TextStyle::Button; let font = &ui.fonts()[text_style]; let galley = font.layout_multiline(text, ui.available().width()); - let interact = ui.reserve_space( - ui.style().button_padding - + vec2(ui.style().start_icon_width, 0.0) - + galley.size - + ui.style().button_padding, - Some(id), - ); + let size = ui.style().button_padding + + vec2(ui.style().start_icon_width, 0.0) + + galley.size + + ui.style().button_padding; + let rect = ui.allocate_space(size); + let interact = ui.interact(rect, id, Sense::click()); let text_cursor = interact.rect.min + ui.style().button_padding + vec2(ui.style().start_icon_width, 0.0); @@ -461,27 +461,25 @@ impl Widget for Separator { let available_space = ui.available_finite().size(); - let (points, interact) = match ui.layout().dir() { + let (points, rect) = match ui.layout().dir() { Direction::Horizontal => { - let interact = ui.reserve_space(vec2(min_spacing, available_space.y), None); - let r = &interact.rect; + let rect = ui.allocate_space(vec2(min_spacing, available_space.y)); ( [ - pos2(r.center().x, r.top() - extra), - pos2(r.center().x, r.bottom() + extra), + pos2(rect.center().x, rect.top() - extra), + pos2(rect.center().x, rect.bottom() + extra), ], - interact, + rect, ) } Direction::Vertical => { - let interact = ui.reserve_space(vec2(available_space.x, min_spacing), None); - let r = &interact.rect; + let rect = ui.allocate_space(vec2(available_space.x, min_spacing)); ( [ - pos2(r.left() - extra, r.center().y), - pos2(r.right() + extra, r.center().y), + pos2(rect.left() - extra, rect.center().y), + pos2(rect.right() + extra, rect.center().y), ], - interact, + rect, ) } }; @@ -490,6 +488,6 @@ impl Widget for Separator { color: color, width: line_width, }); - ui.response(interact) + ui.response(ui.interact_hover(rect)) } } diff --git a/emigui/src/widgets/slider.rs b/emigui/src/widgets/slider.rs index 071c1af8..95801119 100644 --- a/emigui/src/widgets/slider.rs +++ b/emigui/src/widgets/slider.rs @@ -117,7 +117,7 @@ impl<'a> Widget for Slider<'a> { if text_on_top { let galley = font.layout_single_line(full_text); - let pos = ui.reserve_space(galley.size, None).rect.min; + let pos = ui.allocate_space(galley.size).min; ui.add_galley(pos, galley, text_style, text_color); slider_sans_text.ui(ui) } else { @@ -140,13 +140,12 @@ impl<'a> Widget for Slider<'a> { let id = self.id.unwrap_or_else(|| ui.make_position_id()); - let interact = ui.reserve_space( - Vec2 { - x: ui.available().width(), - y: height, - }, - Some(id), - ); + let size = Vec2 { + x: ui.available().width(), + y: height, + }; + let rect = ui.allocate_space(size); + let interact = ui.interact(rect, id, Sense::click_and_drag()); let left = interact.rect.left() + handle_radius; let right = interact.rect.right() - handle_radius; diff --git a/emigui/src/widgets/text_edit.rs b/emigui/src/widgets/text_edit.rs index 3bc73afd..d4d35d53 100644 --- a/emigui/src/widgets/text_edit.rs +++ b/emigui/src/widgets/text_edit.rs @@ -71,7 +71,8 @@ impl<'t> Widget for TextEdit<'t> { font.layout_single_line(text.clone()) }; let desired_size = galley.size.max(vec2(available_width, line_spacing)); - let interact = ui.reserve_space(desired_size, Some(id)); + let rect = ui.allocate_space(desired_size); + let interact = ui.interact(rect, id, Sense::click_and_drag()); // TODO: implement drag-select if interact.clicked { ui.request_kb_focus(id);