Skip to content

Commit

Permalink
Add set-selection-offsets function to TextInput, TextEdit, and Line…
Browse files Browse the repository at this point in the history
…Edit (#4197)

The function accepts two arguments that specify the start and the end of the text to select.

Fixes #4164
  • Loading branch information
BrandonXLF committed Jan 6, 2024
1 parent ed54581 commit 3e89406
Show file tree
Hide file tree
Showing 23 changed files with 233 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -22,6 +22,7 @@ All notable changes to this project are documented in this file.
as well.
- Errors are thrown when trying to modify properties that must be known at compile time.
- Added `colorize-icon` property to `Button`.
- Added `set-selection-offsets(int, int)` to `TextInput`, `LineEdit`, and `TextEdit`.

### C++

Expand Down
1 change: 1 addition & 0 deletions docs/reference/src/language/builtins/elements.md
Expand Up @@ -648,6 +648,7 @@ When not part of a layout, its width or height defaults to 100% of the parent el
### Functions

- **`focus()`** Call this function to focus the text input and make it receive future keyboard events.
- **`set-selection-offsets(int, int)`** Selects the text between two UTF-8 offsets.
- **`select-all()`** Selects all text.
- **`clear-selection()`** Clears the selection.
- **`copy()`** Copies the selected text to the clipboard.
Expand Down
1 change: 1 addition & 0 deletions docs/reference/src/language/widgets/lineedit.md
Expand Up @@ -18,6 +18,7 @@ a widget able to handle several lines of text.
### Functions

- **`focus()`** Call this function to focus the LineEdit and make it receive future keyboard events.
- **`set-selection-offsets(int, int)`** Selects the text between two UTF-8 offsets.
- **`select-all()`** Selects all text.
- **`clear-selection()`** Clears the selection.
- **`copy()`** Copies the selected text to the clipboard.
Expand Down
1 change: 1 addition & 0 deletions docs/reference/src/language/widgets/textedit.md
Expand Up @@ -19,6 +19,7 @@ shortcut will be implemented in a future version: <https://github.com/slint-ui/s
### Functions

- **`focus()`** Call this function to focus the TextEdit and make it receive future keyboard events.
- **`set-selection-offsets(int, int)`** Selects the text between two UTF-8 offsets.
- **`select-all()`** Selects all text.
- **`clear-selection()`** Clears the selection.
- **`copy()`** Copies the selected text to the clipboard.
Expand Down
1 change: 1 addition & 0 deletions internal/compiler/builtins.slint
Expand Up @@ -184,6 +184,7 @@ export component TextInput {
out property <string> preedit-text;
//-default_size_binding:expands_to_parent_geometry
//-accepts_focus
function set-selection-offsets(start: int, end: int) {}
function select-all() {}
function clear-selection() {}
function cut() {}
Expand Down
7 changes: 7 additions & 0 deletions internal/compiler/expression_tree.rs
Expand Up @@ -40,6 +40,7 @@ pub enum BuiltinFunction {
SetFocusItem,
ShowPopupWindow,
ClosePopupWindow,
SetSelectionOffsets,
/// A function that belongs to an item (such as TextInput's select-all function).
ItemMemberFunction(String),
/// the "42".to_float()
Expand Down Expand Up @@ -134,6 +135,10 @@ impl BuiltinFunction {
args: vec![Type::ElementReference],
}
}
BuiltinFunction::SetSelectionOffsets => Type::Function {
return_type: Box::new(Type::Void),
args: vec![Type::ElementReference, Type::Int32, Type::Int32],
},
BuiltinFunction::ItemMemberFunction(..) => Type::Function {
return_type: Box::new(Type::Void),
args: vec![Type::ElementReference],
Expand Down Expand Up @@ -248,6 +253,7 @@ impl BuiltinFunction {
| BuiltinFunction::ATan => true,
BuiltinFunction::SetFocusItem => false,
BuiltinFunction::ShowPopupWindow | BuiltinFunction::ClosePopupWindow => false,
BuiltinFunction::SetSelectionOffsets => false,
BuiltinFunction::ItemMemberFunction(..) => false,
BuiltinFunction::StringToFloat | BuiltinFunction::StringIsFloat => true,
BuiltinFunction::ColorBrighter
Expand Down Expand Up @@ -301,6 +307,7 @@ impl BuiltinFunction {
| BuiltinFunction::ATan => true,
BuiltinFunction::SetFocusItem => false,
BuiltinFunction::ShowPopupWindow | BuiltinFunction::ClosePopupWindow => false,
BuiltinFunction::SetSelectionOffsets => false,
BuiltinFunction::ItemMemberFunction(..) => false,
BuiltinFunction::StringToFloat | BuiltinFunction::StringIsFloat => true,
BuiltinFunction::ColorBrighter
Expand Down
13 changes: 13 additions & 0 deletions internal/compiler/generator/cpp.rs
Expand Up @@ -3097,6 +3097,19 @@ fn compile_builtin_function_call(
let window = access_window_field(ctx);
format!("{window}.close_popup()")
}
BuiltinFunction::SetSelectionOffsets => {
if let [llr::Expression::PropertyReference(pr), from, to] = arguments {
let item = access_member(pr, ctx);
let item_rc = access_item_rc(pr, ctx);
let window = access_window_field(ctx);
let start = compile_expression(from, ctx);
let end = compile_expression(to, ctx);

format!("slint_textinput_set_selection_offsets(&{item}, &{window}.handle(), &{item_rc}, static_cast<int>({start}), static_cast<int>({end}))")
} else {
panic!("internal error: invalid args to set-selection-offsets {:?}", arguments)
}
}
BuiltinFunction::ItemMemberFunction(name) => {
if let [llr::Expression::PropertyReference(pr)] = arguments {
let item = access_member(pr, ctx);
Expand Down
15 changes: 15 additions & 0 deletions internal/compiler/generator/rust.rs
Expand Up @@ -2366,6 +2366,21 @@ fn compile_builtin_function_call(
sp::WindowInner::from_pub(#window_adapter_tokens.window()).close_popup()
)
}
BuiltinFunction::SetSelectionOffsets => {
if let [llr::Expression::PropertyReference(pr), from, to] = arguments {
let item = access_member(pr, ctx);
let item_rc = access_item_rc(pr, ctx);
let window_adapter_tokens = access_window_adapter_field(ctx);
let start = compile_expression(from, ctx);
let end = compile_expression(to, ctx);

quote!(
#item.set_selection_offsets(#window_adapter_tokens, #item_rc, #start as i32, #end as i32)
)
} else {
panic!("internal error: invalid args to set-selection-offsets {:?}", arguments)
}
}
BuiltinFunction::ItemMemberFunction(name) => {
if let [Expression::PropertyReference(pr)] = arguments {
let item = access_member(pr, ctx);
Expand Down
1 change: 1 addition & 0 deletions internal/compiler/llr/optim_passes/inline_expressions.rs
Expand Up @@ -88,6 +88,7 @@ fn builtin_function_cost(function: &BuiltinFunction) -> isize {
BuiltinFunction::Pow => 10,
BuiltinFunction::SetFocusItem => isize::MAX,
BuiltinFunction::ShowPopupWindow | BuiltinFunction::ClosePopupWindow => isize::MAX,
BuiltinFunction::SetSelectionOffsets => isize::MAX,
BuiltinFunction::ItemMemberFunction(..) => isize::MAX,
BuiltinFunction::StringToFloat => 50,
BuiltinFunction::StringIsFloat => 50,
Expand Down
15 changes: 15 additions & 0 deletions internal/compiler/typeregister.rs
Expand Up @@ -335,6 +335,21 @@ impl TypeRegister {
_ => unreachable!(),
};

match &mut register.elements.get_mut("TextInput").unwrap() {
ElementType::Builtin(ref mut b) => {
let text_input = Rc::get_mut(b).unwrap();
text_input.properties.insert(
"set-selection-offsets".into(),
BuiltinPropertyInfo::new(BuiltinFunction::SetSelectionOffsets.ty()),
);
text_input
.member_functions
.insert("set-selection-offsets".into(), BuiltinFunction::SetSelectionOffsets);
}

_ => unreachable!(),
};

register
}

Expand Down
4 changes: 4 additions & 0 deletions internal/compiler/widgets/common/common.slint
Expand Up @@ -12,6 +12,10 @@ export component TextEdit inherits ScrollView {

callback edited(/* text */ string);

public function set-selection-offsets(start: int, end: int) {
i-text-input.set-selection-offsets(start, end);
}

public function select-all() {
i-text-input.select-all();
}
Expand Down
4 changes: 4 additions & 0 deletions internal/compiler/widgets/common/lineedit-base.slint
Expand Up @@ -20,6 +20,10 @@ export component LineEditBase inherits Rectangle {
callback accepted( /* text */ string);
callback edited(/* text */ string);

public function set-selection-offsets(start: int, end: int) {
i-text-input.set-selection-offsets(start, end);
}

public function select-all() {
i-text-input.select-all();
}
Expand Down
4 changes: 4 additions & 0 deletions internal/compiler/widgets/cupertino-base/lineedit.slint
Expand Up @@ -18,6 +18,10 @@ export component LineEdit {
callback accepted <=> i-base.accepted;
callback edited <=> i-base.edited;

public function set-selection-offsets(start: int, end: int) {
i-base.set-selection-offsets(start, end);
}

public function select-all() {
i-base.select-all();
}
Expand Down
4 changes: 4 additions & 0 deletions internal/compiler/widgets/cupertino-base/textedit.slint
Expand Up @@ -78,6 +78,10 @@ export component TextEdit {

callback edited(/* text */ string);

public function set-selection-offsets(start: int, end: int) {
i-text-input.set-selection-offsets(start, end);
}

public function select-all() {
i-text-input.select-all();
}
Expand Down
4 changes: 4 additions & 0 deletions internal/compiler/widgets/fluent-base/lineedit.slint
Expand Up @@ -16,6 +16,10 @@ export component LineEdit {

callback accepted <=> i-base.accepted;
callback edited <=> i-base.edited;

public function set-selection-offsets(start: int, end: int) {
i-base.set-selection-offsets(start, end);
}

public function select-all() {
i-base.select-all();
Expand Down
4 changes: 4 additions & 0 deletions internal/compiler/widgets/fluent-base/textedit.slint
Expand Up @@ -21,6 +21,10 @@ export component TextEdit {

callback edited(/* text */ string);

public function set-selection-offsets(start: int, end: int) {
i-text-input.set-selection-offsets(start, end);
}

public function select-all() {
i-text-input.select-all();
}
Expand Down
4 changes: 4 additions & 0 deletions internal/compiler/widgets/material-base/lineedit.slint
Expand Up @@ -18,6 +18,10 @@ export component LineEdit {
callback accepted <=> i-base.accepted;
callback edited <=> i-base.edited;

public function set-selection-offsets(start: int, end: int) {
i-base.set-selection-offsets(start, end);
}

public function select-all() {
i-base.select-all();
}
Expand Down
4 changes: 4 additions & 0 deletions internal/compiler/widgets/qt/lineedit.slint
Expand Up @@ -17,6 +17,10 @@ export component LineEdit {
callback accepted <=> inner.accepted;
callback edited <=> inner.edited;

public function set-selection-offsets(start: int, end: int) {
inner.set-selection-offsets(start, end);
}

public function select-all() {
inner.select-all();
}
Expand Down
35 changes: 35 additions & 0 deletions internal/core/items/text.rs
Expand Up @@ -1034,6 +1034,21 @@ impl TextInput {
self.delete_selection(window_adapter, self_rc);
}

pub fn set_selection_offsets(
self: Pin<&Self>,
window_adapter: &Rc<dyn WindowAdapter>,
self_rc: &ItemRc,
start: i32,
end: i32,
) {
let text = self.text();
let safe_start = safe_byte_offset(start, &text);
let safe_end = safe_byte_offset(end, &text);

self.as_ref().anchor_position_byte_offset.set(safe_start as i32);
self.set_cursor_position(safe_end as i32, true, window_adapter, self_rc);
}

pub fn select_all(self: Pin<&Self>, window_adapter: &Rc<dyn WindowAdapter>, self_rc: &ItemRc) {
self.move_cursor(
TextCursorDirection::StartOfText,
Expand Down Expand Up @@ -1265,6 +1280,26 @@ fn next_word_boundary(text: &str, last_cursor_pos: usize) -> usize {
.map_or(text.len(), |(offset, slice)| offset + slice.len())
}

#[cfg(feature = "ffi")]
#[no_mangle]
pub unsafe extern "C" fn slint_textinput_set_selection_offsets(
text_input: *const TextInput,
window_adapter: *const crate::window::ffi::WindowAdapterRcOpaque,
self_component: &vtable::VRc<crate::item_tree::ItemTreeVTable>,
self_index: u32,
start: i32,
end: i32,
) {
let window_adapter = &*(window_adapter as *const Rc<dyn WindowAdapter>);
let self_rc = ItemRc::new(self_component.clone(), self_index);
Pin::new_unchecked(&*text_input).as_ref().set_selection_offsets(
window_adapter,
&self_rc,
start,
end,
);
}

#[cfg(feature = "ffi")]
#[no_mangle]
pub unsafe extern "C" fn slint_textinput_select_all(
Expand Down
55 changes: 54 additions & 1 deletion internal/interpreter/eval.rs
Expand Up @@ -610,6 +610,59 @@ fn call_builtin_function(

Value::Void
}
BuiltinFunction::SetSelectionOffsets => {
if arguments.len() != 3 {
panic!("internal error: incorrect argument count to select range function call")
}
let component = match local_context.component_instance {
ComponentInstance::InstanceRef(c) => c,
ComponentInstance::GlobalComponent(_) => {
panic!("Cannot invoke member function on item from a global component")
}
};
if let Expression::ElementReference(element) = &arguments[0] {
generativity::make_guard!(guard);

let elem = element.upgrade().unwrap();
let enclosing_component = enclosing_component_for_element(&elem, component, guard);
let description = enclosing_component.description;
let item_info = &description.items[elem.borrow().id.as_str()];
let item_ref =
unsafe { item_info.item_from_item_tree(enclosing_component.as_ptr()) };

let item_comp = enclosing_component.self_weak().get().unwrap().upgrade().unwrap();
let item_rc = corelib::items::ItemRc::new(
vtable::VRc::into_dyn(item_comp),
item_info.item_index(),
);

let window_adapter = component.window_adapter();

// TODO: Make this generic through RTTI
if let Some(textinput) =
ItemRef::downcast_pin::<corelib::items::TextInput>(item_ref)
{
let start: i32 =
eval_expression(&arguments[1], local_context).try_into().expect(
"internal error: second argument to set-selection-offsets must be an integer",
);
let end: i32 = eval_expression(&arguments[2], local_context).try_into().expect(
"internal error: third argument to set-selection-offsets must be an integer",
);

textinput.set_selection_offsets(&window_adapter, &item_rc, start, end);
} else {
panic!(
"internal error: member function called on element that doesn't have it: {}",
elem.borrow().original_name()
)
}

Value::Void
} else {
panic!("internal error: first argument to set-selection-offsets must be an element")
}
}
BuiltinFunction::ItemMemberFunction(name) => {
if arguments.len() != 1 {
panic!("internal error: incorrect argument count to item member function call")
Expand Down Expand Up @@ -659,7 +712,7 @@ fn call_builtin_function(

Value::Void
} else {
panic!("internal error: argument to TextInputSelectAll must be an element")
panic!("internal error: argument to set-selection-offsetsAll must be an element")
}
}
BuiltinFunction::StringIsFloat => {
Expand Down

0 comments on commit 3e89406

Please sign in to comment.