Skip to content

Commit b4b1268

Browse files
ickshonpemockersf
authored andcommitted
text input selection radius (#24307)
# Objective Allow rounded corners for text input selection regions. Fixes #23952 ## Solution * Added a `selection_radius` field to `TextCursorStyle`, defaults to `0.`. * In `extract_text_cursor` set a corner radius for each selection rect, after clamping for short and inner corners. --- Left rounded inner corners for a follow up, as they are significantly more complicated to implement. ## Testing Added an option to set the selection radius to the `multiline_text_input` example: ``` cargo run --example multiline_text_input --features="system_clipboard" ``` <img width="400" alt="image" src="https://github.com/user-attachments/assets/a10bb552-ac03-4e06-a287-53c57e5c2f45" /> ## Showcase <img width="400" alt="rounded" src="https://github.com/user-attachments/assets/79d59c66-7fb0-4ad4-8a17-308c755c1f23" /> <img width="400" alt="r2" src="https://github.com/user-attachments/assets/a3dfd8a6-f85f-4534-ba61-4a8c74e9793d" /> <img width="400" alt="r3" src="https://github.com/user-attachments/assets/78e4d863-198e-4ee0-ad33-22e7cde7016a" />
1 parent 855af18 commit b4b1268

3 files changed

Lines changed: 132 additions & 2 deletions

File tree

crates/bevy_text/src/cursor.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ pub struct TextCursorStyle {
2323
pub unfocused_selection_color: Color,
2424
/// If some, overrides the color of selected text
2525
pub selected_text_color: Option<Color>,
26+
/// Corner radius of selection highlight rectangles, normalized relative to the selection height.
27+
///
28+
/// Values are clamped between `0.0` and `0.5`.
29+
pub selection_radius: f32,
2630
}
2731

2832
impl Default for TextCursorStyle {
@@ -32,6 +36,7 @@ impl Default for TextCursorStyle {
3236
selection_color: Color::from(SKY_300),
3337
unfocused_selection_color: Color::from(SKY_400),
3438
selected_text_color: None,
39+
selection_radius: 0.,
3540
}
3641
}
3742
}

crates/bevy_ui_render/src/text.rs

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,50 @@ pub fn extract_text_cursor(
9292

9393
if !text_layout_info.selection_rects.is_empty() && !sc.is_fully_transparent() {
9494
let selection_color = sc.to_linear();
95+
let selection_radius = cursor_style.selection_radius.clamp(0.0, 0.5);
96+
97+
for (prev, selection, next) in
98+
text_layout_info
99+
.selection_rects
100+
.iter()
101+
.enumerate()
102+
.map(|(i, current)| {
103+
(
104+
i.checked_sub(1)
105+
.map(|i| text_layout_info.selection_rects[i]),
106+
current,
107+
text_layout_info.selection_rects.get(i + 1),
108+
)
109+
})
110+
{
111+
let radius = selection.height() * selection_radius;
112+
let mut border_radius = ResolvedBorderRadius {
113+
top_left: radius,
114+
top_right: radius,
115+
bottom_right: radius,
116+
bottom_left: radius,
117+
};
118+
119+
if let Some(prev) = prev {
120+
if selection.min.x <= prev.max.x {
121+
border_radius.top_left = (prev.min.x - selection.min.x).clamp(0., radius);
122+
}
123+
if prev.min.x <= selection.max.x {
124+
border_radius.top_right = (selection.max.x - prev.max.x).clamp(0., radius);
125+
}
126+
}
127+
128+
if let Some(next) = next {
129+
if selection.min.x <= next.max.x {
130+
border_radius.bottom_left =
131+
(next.min.x - selection.min.x).clamp(0., radius);
132+
}
133+
if next.min.x <= selection.max.x {
134+
border_radius.bottom_right =
135+
(selection.max.x - next.max.x).clamp(0., radius);
136+
}
137+
}
95138

96-
for selection in text_layout_info.selection_rects.iter() {
97139
extracted_uinodes.uinodes.push(ExtractedUiNode {
98140
render_entity: commands.spawn(TemporaryRenderEntity).id(),
99141
z_order: stack_index.0 as f32 + stack_z_offsets::TEXT_SELECTION,
@@ -111,7 +153,7 @@ pub fn extract_text_cursor(
111153
flip_x: false,
112154
flip_y: false,
113155
border: BorderRect::default(),
114-
border_radius: ResolvedBorderRadius::default(),
156+
border_radius,
115157
node_type: NodeType::Rect,
116158
},
117159
main_entity: entity.into(),

examples/ui/text/multiline_text_input.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ struct VisibleLinesInput;
2424
#[derive(Component)]
2525
struct FontSizeInput;
2626

27+
#[derive(Component)]
28+
struct SelectionRadiusInput;
29+
2730
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
2831
commands.spawn(Camera2d);
2932

@@ -264,6 +267,86 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
264267
FontSize::Px(font_size.clamp(5., 50.));
265268
},
266269
);
270+
271+
parent
272+
.spawn((
273+
Node {
274+
flex_direction: FlexDirection::Row,
275+
column_gap: px(10.),
276+
..default()
277+
},
278+
children![
279+
(
280+
Text::new("corner radius:"),
281+
TextFont {
282+
font: asset_server.load("fonts/FiraMono-Medium.ttf").into(),
283+
font_size: FontSize::Px(30.),
284+
..default()
285+
},
286+
),
287+
(
288+
Node {
289+
width: px(100.),
290+
border: px(2.).all(),
291+
..default()
292+
},
293+
TextFont {
294+
font: asset_server.load("fonts/FiraMono-Medium.ttf").into(),
295+
font_size: FontSize::Px(30.),
296+
..default()
297+
},
298+
TextLayout {
299+
justify: Justify::End,
300+
..default()
301+
},
302+
BackgroundColor(DARK_SLATE_GRAY.into()),
303+
BorderColor::all(SLATE_300),
304+
EditableText::new("0"),
305+
EditableTextFilter::new(|c| c.is_ascii_digit() || c == '.'),
306+
TextCursorStyle {
307+
color: Color::WHITE,
308+
selected_text_color: Some(Color::BLACK),
309+
..default()
310+
},
311+
SelectionRadiusInput,
312+
TabIndex(2),
313+
)
314+
],
315+
))
316+
.observe(
317+
|on: On<FocusedInput<KeyboardInput>>,
318+
radius_input_query: Query<
319+
&EditableText,
320+
With<SelectionRadiusInput>,
321+
>,
322+
mut cursor_style: Single<
323+
&mut TextCursorStyle,
324+
With<MultilineInput>,
325+
>| {
326+
if !(on.input.state.is_pressed()
327+
&& on.input.logical_key == Key::Enter)
328+
{
329+
return;
330+
}
331+
332+
let Ok(input) = radius_input_query.get(on.original_event_target())
333+
else {
334+
return;
335+
};
336+
337+
let mut output = String::new();
338+
output.reserve(input.value().into_iter().map(str::len).sum());
339+
for sub_str in input.value() {
340+
output.push_str(sub_str);
341+
}
342+
343+
let Ok(radius) = output.parse::<f32>() else {
344+
return;
345+
};
346+
347+
cursor_style.selection_radius = radius.clamp(0., 0.5);
348+
},
349+
);
267350
});
268351
});
269352
}

0 commit comments

Comments
 (0)