🐛 Fix gradients and text selection (#10941)

* 🐛 Add missing text fills

*  Keep text leaf selected while editing
This commit is contained in:
Elena Torró 2026-07-30 12:11:53 +02:00 committed by GitHub
parent 03f9393200
commit a722a33503
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 140 additions and 13 deletions

View File

@ -12,7 +12,9 @@ pub fn render_overlay(
editor_state: &TextEditorState,
shape: &Shape,
) {
if !editor_state.has_focus {
let has_selection = editor_state.selection.is_selection();
if !editor_state.has_focus && !has_selection {
return;
}
@ -25,12 +27,12 @@ pub fn render_overlay(
canvas.scale((zoom, zoom));
canvas.translate((-viewbox.area.left, -viewbox.area.top));
if editor_state.selection.is_selection() {
if has_selection {
// With an active selection there is no blinking caret (the caret is one
// end of the selection); drawing it would make it toggle on top of the
// highlight while the selection is held.
render_selection(canvas, editor_state, text_content, shape);
} else if editor_state.cursor_visible {
} else if editor_state.has_focus && editor_state.cursor_visible {
render_cursor(canvas, zoom, options.dpr, editor_state, text_content, shape);
}

View File

@ -42,6 +42,29 @@ impl Gradient {
self.offsets.extend(offsets);
}
pub fn start(&self) -> (f32, f32) {
self.start
}
pub fn end(&self) -> (f32, f32) {
self.end
}
pub fn opacity(&self) -> u8 {
self.opacity
}
pub fn width(&self) -> f32 {
self.width
}
pub fn stops(&self) -> impl Iterator<Item = (Color, f32)> + '_ {
self.colors
.iter()
.copied()
.zip(self.offsets.iter().copied())
}
pub fn to_linear_shader(&self, rect: &Rect) -> Option<skia::Shader> {
let start = (
rect.left + self.start.0 * rect.width(),
@ -126,6 +149,14 @@ impl ImageFill {
pub fn keep_aspect_ratio(&self) -> bool {
self.keep_aspect_ratio
}
pub fn width(&self) -> i32 {
self.width
}
pub fn height(&self) -> i32 {
self.height
}
}
#[derive(Debug, Clone, PartialEq, Copy)]

View File

@ -385,11 +385,15 @@ impl TextEditorState {
}
pub fn focus(&mut self, shape_id: Uuid) {
let same_shape = self.active_shape_id == Some(shape_id);
self.has_focus = true;
self.active_shape_id = Some(shape_id);
self.cursor_visible = true;
self.last_blink_time_ms = 0.0;
self.selection.reset();
if !same_shape {
self.selection.reset();
}
self.is_pointer_selection_active = false;
self.is_overtype_mode = false;
self.pending_events.clear();

View File

@ -48,15 +48,15 @@ impl TryFrom<&shapes::Fill> for RawFillData {
| (color.b() as u32),
}))
}
shapes::Fill::LinearGradient(_) => {
Err("LinearGradient serialization is not implemented".to_string())
}
shapes::Fill::RadialGradient(_) => {
Err("RadialGradient serialization is not implemented".to_string())
}
shapes::Fill::Image(_) => {
Err("Image fill serialization is not implemented".to_string())
}
shapes::Fill::LinearGradient(linear_gradient) => Ok(RawFillData::Linear(
gradient::RawGradientData::from(linear_gradient),
)),
shapes::Fill::RadialGradient(radial_gradient) => Ok(RawFillData::Radial(
gradient::RawGradientData::from(radial_gradient),
)),
shapes::Fill::Image(image_fill) => Ok(RawFillData::Image(
image::RawImageFillData::from(image_fill),
)),
}
}
}
@ -155,4 +155,36 @@ mod tests {
RawFillData::Solid(solid::RawSolidData { color: 0xfffabada })
);
}
#[test]
fn test_gradient_fill_round_trip() {
let gradient = shapes::Gradient::new(
(0.0, 0.5),
(1.0, 0.5),
0x80,
0.25,
&[
(shapes::Color::from(0xfffabada), 0.0),
(shapes::Color::from(0xff00ff00), 1.0),
],
);
let fill = shapes::Fill::LinearGradient(gradient.clone());
let raw_fill = RawFillData::try_from(&fill).expect("gradient must be serializable");
let bytes = <[u8; RAW_FILL_DATA_SIZE]>::from(raw_fill);
assert_eq!(bytes[0], 0x01);
assert_eq!(shapes::Fill::from(RawFillData::from(bytes)), fill);
}
#[test]
fn test_image_fill_round_trip() {
let image_fill = shapes::ImageFill::new(crate::uuid::Uuid::nil(), 0x80, 300, 200, true);
let fill = shapes::Fill::Image(image_fill);
let raw_fill = RawFillData::try_from(&fill).expect("image fill must be serializable");
let bytes = <[u8; RAW_FILL_DATA_SIZE]>::from(raw_fill);
assert_eq!(bytes[0], 0x03);
assert_eq!(shapes::Fill::from(RawFillData::from(bytes)), fill);
}
}

View File

@ -44,6 +44,41 @@ impl RawStopData {
}
}
impl From<&Gradient> for RawGradientData {
fn from(gradient: &Gradient) -> Self {
let mut stops = [RawStopData {
color: 0,
offset: 0.0,
}; MAX_GRADIENT_STOPS];
let mut stop_count: u8 = 0;
for (index, (color, offset)) in gradient.stops().take(MAX_GRADIENT_STOPS).enumerate() {
stops[index] = RawStopData {
color: ((color.a() as u32) << 24)
| ((color.r() as u32) << 16)
| ((color.g() as u32) << 8)
| (color.b() as u32),
offset,
};
stop_count += 1;
}
let (start_x, start_y) = gradient.start();
let (end_x, end_y) = gradient.end();
RawGradientData {
start_x,
start_y,
end_x,
end_y,
opacity: gradient.opacity(),
width: gradient.width(),
stop_count,
stops,
}
}
}
impl From<RawGradientData> for Gradient {
fn from(raw_gradient: RawGradientData) -> Self {
let stops = raw_gradient

View File

@ -48,6 +48,29 @@ pub struct RawImageFillData {
height: i32,
}
impl From<&ImageFill> for RawImageFillData {
fn from(image_fill: &ImageFill) -> Self {
let id = image_fill.id();
let (a, b, c, d) = crate::utils::uuid_to_u32_quartet(&id);
let flags = if image_fill.keep_aspect_ratio() {
FLAG_KEEP_ASPECT_RATIO
} else {
0
};
Self {
a,
b,
c,
d,
opacity: image_fill.opacity(),
flags,
width: image_fill.width(),
height: image_fill.height(),
}
}
}
impl From<RawImageFillData> for ImageFill {
fn from(value: RawImageFillData) -> Self {
let id = uuid_from_u32_quartet(value.a, value.b, value.c, value.d);