🔧 Text Editor Serialization WIP

This commit is contained in:
Elena Torro 2026-03-17 15:37:56 +01:00
parent 1975e3b0e3
commit 1992dc5678
7 changed files with 510 additions and 55 deletions

View File

@ -154,6 +154,229 @@
(mem/free)
text)))))
;; ---------------------------------------------------------------------------
;; Binary layout constants for TextEditorStyles (must match text_editor.rs)
;; ---------------------------------------------------------------------------
(def ^:const STYLES-HEADER-SIZE 120)
;; RAW_FILL_DATA_SIZE = 4 (tag+padding) + RawGradientData (largest variant)
;; RawGradientData = 28 header + 16 stops × 8 bytes = 156
(def ^:const RAW-FILL-DATA-SIZE 160)
;; MultipleState enum values
(def ^:const MULTIPLE-UNDEFINED 0)
(def ^:const MULTIPLE-SINGLE 1)
(def ^:const MULTIPLE-MULTIPLE 2)
(def ^:private vertical-align-map
{0 :top 1 :center 2 :bottom})
(def ^:private text-align-map
{0 :left 1 :center 2 :right 3 :justify})
(def ^:private text-direction-map
{0 :ltr 1 :rtl})
(def ^:private text-decoration-map
{0 nil 1 :underline 2 :line-through 3 :overline})
(def ^:private text-transform-map
{0 nil 1 :uppercase 2 :lowercase 3 :capitalize})
(def ^:private font-style-map
{0 :normal 1 :italic})
(defn- read-multiple
"Read a Multiple<T> field from a DataView. Returns :mixed when Multiple,
nil when Undefined, or calls value-fn to read the value when Single."
[dview state-offset value-fn]
(let [state (.getUint32 dview state-offset true)]
(case state
1 (value-fn) ;; Single
2 :mixed ;; Multiple
nil))) ;; Undefined
(defn- u32-argb->hex
"Convert u32 ARGB to #RRGGBB hex string."
[argb]
(let [r (bit-and (unsigned-bit-shift-right argb 16) 0xFF)
g (bit-and (unsigned-bit-shift-right argb 8) 0xFF)
b (bit-and argb 0xFF)]
(str "#"
(.padStart (.toString r 16) 2 "0")
(.padStart (.toString g 16) 2 "0")
(.padStart (.toString b 16) 2 "0"))))
(defn- u32-argb->opacity
"Extract normalized opacity (0.0-1.0) from u32 ARGB."
[argb]
(/ (bit-and (unsigned-bit-shift-right argb 24) 0xFF) 255.0))
(defn- read-gradient-stops
"Read gradient stops from DataView at the given byte offset."
[dview base-offset stop-count]
(let [stops-offset (+ base-offset 28)] ;; stops start at byte 28 within gradient data
(into []
(map (fn [i]
(let [stop-off (+ stops-offset (* i 8))
color (.getUint32 dview stop-off true)
offset (.getFloat32 dview (+ stop-off 4) true)]
{:color (u32-argb->hex color)
:opacity (u32-argb->opacity color)
:offset offset})))
(range stop-count))))
(defn- read-fill-from-dview
"Read a single RawFillData entry from DataView at the given byte offset.
Returns a fill map in the standard Penpot text content format."
[dview fill-offset]
(let [tag (.getUint8 dview fill-offset)]
(case tag
;; Solid fill
0 (let [color (.getUint32 dview (+ fill-offset 4) true)]
{:fill-color (u32-argb->hex color)
:fill-opacity (u32-argb->opacity color)})
;; Linear gradient
1 (let [base (+ fill-offset 4)
stop-count (.getUint8 dview (+ base 24))]
{:fill-color-gradient
{:type :linear
:start-x (.getFloat32 dview base true)
:start-y (.getFloat32 dview (+ base 4) true)
:end-x (.getFloat32 dview (+ base 8) true)
:end-y (.getFloat32 dview (+ base 12) true)
:width (.getFloat32 dview (+ base 20) true)
:stops (read-gradient-stops dview base stop-count)}
:fill-opacity (/ (.getUint8 dview (+ base 16)) 255.0)})
;; Radial gradient
2 (let [base (+ fill-offset 4)
stop-count (.getUint8 dview (+ base 24))]
{:fill-color-gradient
{:type :radial
:start-x (.getFloat32 dview base true)
:start-y (.getFloat32 dview (+ base 4) true)
:end-x (.getFloat32 dview (+ base 8) true)
:end-y (.getFloat32 dview (+ base 12) true)
:width (.getFloat32 dview (+ base 20) true)
:stops (read-gradient-stops dview base stop-count)}
:fill-opacity (/ (.getUint8 dview (+ base 16)) 255.0)})
;; Image fill
3 (let [base (+ fill-offset 4)
a (.getUint32 dview base true)
b (.getUint32 dview (+ base 4) true)
c (.getUint32 dview (+ base 8) true)
d (.getUint32 dview (+ base 12) true)
opacity (.getUint8 dview (+ base 16))
flags (.getUint8 dview (+ base 17))
width (.getInt32 dview (+ base 20) true)
height (.getInt32 dview (+ base 24) true)]
{:fill-image
{:id (uuid/from-unsigned-parts a b c d)
:width width
:height height
:keep-aspect-ratio (not (zero? (bit-and flags 1)))}
:fill-opacity (/ opacity 255.0)})
;; Unknown tag
nil)))
(defn text-editor-get-current-styles
"Read the current text editor styles from WASM. Returns a map with the
style values for the current selection, or nil if no selection/focus.
Multiple<T> fields return :mixed when spans have different values."
[]
(when wasm/context-initialized?
(let [ptr (h/call wasm/internal-module "_text_editor_get_current_styles")]
(when (and ptr (not (zero? ptr)))
(let [heap-u8 (mem/get-heap-u8)
dview (js/DataView. (.-buffer heap-u8) (.-byteOffset heap-u8))
;; Read fills count first to know total size
fills-count (.getUint32 dview (+ ptr 116) true)
;; Read scalar Multiple<T> fields
vertical-align (get vertical-align-map
(.getUint32 dview ptr true)
:top)
text-align
(read-multiple dview (+ ptr 4)
#(get text-align-map (.getUint32 dview (+ ptr 8) true)))
text-direction
(read-multiple dview (+ ptr 12)
#(get text-direction-map (.getUint32 dview (+ ptr 16) true)))
text-decoration
(read-multiple dview (+ ptr 20)
#(get text-decoration-map (.getUint32 dview (+ ptr 24) true)))
text-transform
(read-multiple dview (+ ptr 28)
#(get text-transform-map (.getUint32 dview (+ ptr 32) true)))
font-size
(read-multiple dview (+ ptr 36)
#(.getFloat32 dview (+ ptr 40) true))
font-weight
(read-multiple dview (+ ptr 44)
#(.getInt32 dview (+ ptr 48) true))
line-height
(read-multiple dview (+ ptr 52)
#(.getFloat32 dview (+ ptr 56) true))
letter-spacing
(read-multiple dview (+ ptr 60)
#(.getFloat32 dview (+ ptr 64) true))
font-family
(read-multiple dview (+ ptr 68)
(fn []
(let [a (.getUint32 dview (+ ptr 72) true)
b (.getUint32 dview (+ ptr 76) true)
c (.getUint32 dview (+ ptr 80) true)
d (.getUint32 dview (+ ptr 84) true)]
{:id (uuid/from-unsigned-parts a b c d)
:style (get font-style-map (.getUint32 dview (+ ptr 88) true))
:weight (.getUint32 dview (+ ptr 92) true)})))
font-variant-id
(read-multiple dview (+ ptr 96)
(fn []
(let [a (.getUint32 dview (+ ptr 100) true)
b (.getUint32 dview (+ ptr 104) true)
c (.getUint32 dview (+ ptr 108) true)
d (.getUint32 dview (+ ptr 112) true)]
(uuid/from-unsigned-parts a b c d))))
;; Read fills
fills
(into []
(keep (fn [i]
(read-fill-from-dview dview
(+ ptr STYLES-HEADER-SIZE
(* i RAW-FILL-DATA-SIZE)))))
(range fills-count))]
(mem/free)
{:vertical-align vertical-align
:text-align text-align
:text-direction text-direction
:text-decoration text-decoration
:text-transform text-transform
:font-size font-size
:font-weight font-weight
:line-height line-height
:letter-spacing letter-spacing
:font-family font-family
:font-variant-id font-variant-id
:fills fills})))))
(defn text-editor-get-active-shape-id
[]
(when wasm/context-initialized?

View File

@ -35,6 +35,30 @@ impl Gradient {
gradient
}
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 colors(&self) -> &[Color] {
&self.colors
}
pub fn offsets(&self) -> &[f32] {
&self.offsets
}
fn add_stops(&mut self, stops: &[(Color, f32)]) {
let colors = stops.iter().map(|(color, _)| *color);
let offsets = stops.iter().map(|(_, offset)| *offset);
@ -123,6 +147,14 @@ impl ImageFill {
self.opacity
}
pub fn width(&self) -> i32 {
self.width
}
pub fn height(&self) -> i32 {
self.height
}
pub fn keep_aspect_ratio(&self) -> bool {
self.keep_aspect_ratio
}

View File

@ -30,6 +30,18 @@ impl FontFamily {
Self { id, style, weight }
}
pub fn id(&self) -> Uuid {
self.id
}
pub fn style(&self) -> FontStyle {
self.style
}
pub fn weight(&self) -> u32 {
self.weight
}
pub fn alias(&self) -> String {
format!("{}", self)
}

View File

@ -36,6 +36,39 @@ impl From<RawFillData> for shapes::Fill {
}
}
fn color_to_u32(color: &shapes::Color) -> u32 {
((color.a() as u32) << 24)
| ((color.r() as u32) << 16)
| ((color.g() as u32) << 8)
| (color.b() as u32)
}
fn gradient_to_raw(g: &shapes::Gradient) -> gradient::RawGradientData {
let mut stops = [gradient::RawStopData {
color: 0,
offset: 0.0,
}; gradient::MAX_GRADIENT_STOPS];
let colors = g.colors();
let offsets = g.offsets();
let stop_count = colors.len().min(gradient::MAX_GRADIENT_STOPS);
for i in 0..stop_count {
stops[i] = gradient::RawStopData {
color: color_to_u32(&colors[i]),
offset: offsets[i],
};
}
gradient::RawGradientData {
start_x: g.start().0,
start_y: g.start().1,
end_x: g.end().0,
end_y: g.end().1,
opacity: g.opacity(),
width: g.width(),
stop_count: stop_count as u8,
stops,
}
}
impl TryFrom<&shapes::Fill> for RawFillData {
type Error = String;
@ -43,19 +76,29 @@ impl TryFrom<&shapes::Fill> for RawFillData {
match fill {
shapes::Fill::Solid(shapes::SolidColor(color)) => {
Ok(RawFillData::Solid(solid::RawSolidData {
color: ((color.a() as u32) << 24)
| ((color.r() as u32) << 16)
| ((color.g() as u32) << 8)
| (color.b() as u32),
color: color_to_u32(color),
}))
}
shapes::Fill::LinearGradient(_) => {
Err("LinearGradient serialization is not implemented".to_string())
shapes::Fill::LinearGradient(g) => Ok(RawFillData::Linear(gradient_to_raw(g))),
shapes::Fill::RadialGradient(g) => Ok(RawFillData::Radial(gradient_to_raw(g))),
shapes::Fill::Image(img) => {
let id_bytes: [u8; 16] = img.id().into();
let a = u32::from_le_bytes(id_bytes[0..4].try_into().unwrap());
let b = u32::from_le_bytes(id_bytes[4..8].try_into().unwrap());
let c = u32::from_le_bytes(id_bytes[8..12].try_into().unwrap());
let d = u32::from_le_bytes(id_bytes[12..16].try_into().unwrap());
let flags = if img.keep_aspect_ratio() { 1u8 } else { 0u8 };
Ok(RawFillData::Image(image::RawImageFillData {
a,
b,
c,
d,
opacity: img.opacity(),
flags,
width: img.width(),
height: img.height(),
}))
}
shapes::Fill::RadialGradient(_) => {
Err("RadialGradient serialization is not implemented".to_string())
}
shapes::Fill::Image(_) => Err("Image fill serialization is not implemented".to_string()),
}
}
}

View File

@ -1,20 +1,20 @@
use crate::shapes::{Color, Gradient};
const MAX_GRADIENT_STOPS: usize = 16;
pub(crate) const MAX_GRADIENT_STOPS: usize = 16;
#[derive(Debug, PartialEq, Clone, Copy)]
#[repr(C)]
#[repr(align(4))]
pub struct RawGradientData {
start_x: f32,
start_y: f32,
end_x: f32,
end_y: f32,
opacity: u8,
pub(crate) start_x: f32,
pub(crate) start_y: f32,
pub(crate) end_x: f32,
pub(crate) end_y: f32,
pub(crate) opacity: u8,
// 24-bit padding here, reserved for future use
width: f32,
stop_count: u8,
stops: [RawStopData; MAX_GRADIENT_STOPS],
pub(crate) width: f32,
pub(crate) stop_count: u8,
pub(crate) stops: [RawStopData; MAX_GRADIENT_STOPS],
}
impl RawGradientData {
@ -29,9 +29,9 @@ impl RawGradientData {
#[derive(Debug, PartialEq, Clone, Copy)]
#[repr(C)]
struct RawStopData {
color: u32,
offset: f32,
pub(crate) struct RawStopData {
pub(crate) color: u32,
pub(crate) offset: f32,
}
impl RawStopData {

View File

@ -15,15 +15,15 @@ const IMAGE_HEADER_SIZE: usize = 36; // 32 bytes for IDs + 4 bytes for is_thumbn
#[repr(C)]
#[repr(align(4))]
pub struct RawImageFillData {
a: u32,
b: u32,
c: u32,
d: u32,
opacity: u8,
flags: u8,
pub(crate) a: u32,
pub(crate) b: u32,
pub(crate) c: u32,
pub(crate) d: u32,
pub(crate) opacity: u8,
pub(crate) flags: u8,
// 16-bit padding here, reserved for future use
width: i32,
height: i32,
pub(crate) width: i32,
pub(crate) height: i32,
}
impl From<RawImageFillData> for ImageFill {

View File

@ -2,10 +2,14 @@ use macros::{wasm_error, ToJs};
use crate::math::{Matrix, Point, Rect};
use crate::mem;
use crate::shapes::{Shape, TextContent, TextPositionWithAffinity, Type, VerticalAlign};
use crate::shapes::{
FontStyle, Shape, TextContent, TextDecoration, TextPositionWithAffinity, TextTransform, Type,
VerticalAlign,
};
use crate::state::{TextEditorEvent, TextSelection};
use crate::utils::uuid_from_u32_quartet;
use crate::utils::uuid_to_u32_quartet;
use crate::wasm::fills::write_fills_to_bytes;
use crate::wasm::text::helpers as text_helpers;
use crate::{with_state, with_state_mut, STATE};
use skia_safe::Color;
@ -517,6 +521,87 @@ pub extern "C" fn text_editor_get_cursor_rect() -> *mut u8 {
})
}
/// Serialize a skia TextAlign to its raw u32 representation.
fn text_align_to_raw(align: &skia_safe::textlayout::TextAlign) -> u32 {
use skia_safe::textlayout::TextAlign;
match *align {
TextAlign::Left => 0,
TextAlign::Center => 1,
TextAlign::Right => 2,
TextAlign::Justify => 3,
_ => 0,
}
}
/// Serialize a skia TextDirection to its raw u32 representation.
fn text_direction_to_raw(dir: &skia_safe::textlayout::TextDirection) -> u32 {
use skia_safe::textlayout::TextDirection;
match *dir {
TextDirection::LTR => 0,
TextDirection::RTL => 1,
}
}
/// Serialize an Option<TextDecoration> to its raw u32 representation.
fn text_decoration_to_raw(dec: &Option<TextDecoration>) -> u32 {
match dec.as_ref() {
None => 0,
Some(d) if *d == TextDecoration::UNDERLINE => 1,
Some(d) if *d == TextDecoration::LINE_THROUGH => 2,
Some(d) if *d == TextDecoration::OVERLINE => 3,
_ => 0,
}
}
/// Serialize an Option<TextTransform> to its raw u32 representation.
fn text_transform_to_raw(transform: &Option<TextTransform>) -> u32 {
match transform.as_ref() {
None => 0,
Some(TextTransform::Uppercase) => 1,
Some(TextTransform::Lowercase) => 2,
Some(TextTransform::Capitalize) => 3,
}
}
/// Serialize a FontStyle to its raw u32 representation.
fn font_style_to_raw(style: &FontStyle) -> u32 {
match *style {
FontStyle::Normal => 0,
FontStyle::Italic => 1,
}
}
/// Binary layout for TextEditorStyles (all offsets in bytes, 4-byte aligned):
///
/// Offset Size Field
/// ------ ---- -----
/// 0 4 vertical_align (u32)
/// 4 4 text_align.state (u32)
/// 8 4 text_align.value (u32)
/// 12 4 text_direction.state (u32)
/// 16 4 text_direction.value (u32)
/// 20 4 text_decoration.state (u32)
/// 24 4 text_decoration.value (u32)
/// 28 4 text_transform.state (u32)
/// 32 4 text_transform.value (u32)
/// 36 4 font_size.state (u32)
/// 40 4 font_size.value (f32)
/// 44 4 font_weight.state (u32)
/// 48 4 font_weight.value (i32)
/// 52 4 line_height.state (u32)
/// 56 4 line_height.value (f32)
/// 60 4 letter_spacing.state (u32)
/// 64 4 letter_spacing.value (f32)
/// 68 4 font_family.state (u32)
/// 72 16 font_family.id (Uuid: 4×u32 LE)
/// 88 4 font_family.style (u32)
/// 92 4 font_family.weight (u32)
/// 96 4 font_variant_id.state (u32)
/// 100 16 font_variant_id.value (Uuid: 4×u32 LE)
/// 116 4 fills_count (u32)
/// 120+ var fills (RAW_FILL_DATA_SIZE each, same format as set_shape_fills)
const STYLES_HEADER_SIZE: usize = 120;
#[no_mangle]
pub extern "C" fn text_editor_get_current_styles() -> *mut u8 {
with_state_mut!(state, {
@ -528,38 +613,98 @@ pub extern "C" fn text_editor_get_current_styles() -> *mut u8 {
return std::ptr::null_mut();
}
let Some(shape_id) = state.text_editor_state.active_shape_id else {
let Some(_shape_id) = state.text_editor_state.active_shape_id else {
return std::ptr::null_mut();
};
let Some(shape) = state.shapes.get(&shape_id) else {
return std::ptr::null_mut();
};
let styles = &state.text_editor_state.current_styles;
let Type::Text(text_content) = &shape.shape_type else {
return std::ptr::null_mut();
};
// Serialize fills using the existing RawFillData format
let fill_bytes = write_fills_to_bytes(styles.fills.clone());
let total_size = STYLES_HEADER_SIZE + fill_bytes.len();
let mut bytes = vec![0u8; total_size];
let mut bytes = vec![0u8; 1024];
bytes[0..4].copy_from_slice(&u32::to_le_bytes(state.text_editor_state.current_styles.vertical_align as u32));
bytes[4..8].copy_from_slice(&u32::to_le_bytes(*state.text_editor_state.current_styles.text_align.state() as u32));
bytes[8..12].copy_from_slice(&u32::to_le_bytes(*state.text_editor_state.current_styles.text_direction.state() as u32));
bytes[12..16].copy_from_slice(&u32::to_le_bytes(*state.text_editor_state.current_styles.text_decoration.state() as u32));
bytes[16..20].copy_from_slice(&u32::to_le_bytes(*state.text_editor_state.current_styles.text_transform.state() as u32));
bytes[20..24].copy_from_slice(&u32::to_le_bytes(*state.text_editor_state.current_styles.font_family.state() as u32));
bytes[24..28].copy_from_slice(&u32::to_le_bytes(*state.text_editor_state.current_styles.font_size.state() as u32));
bytes[28..32].copy_from_slice(&u32::to_le_bytes(*state.text_editor_state.current_styles.font_weight.state() as u32));
bytes[32..36].copy_from_slice(&u32::to_le_bytes(*state.text_editor_state.current_styles.font_variant_id.state() as u32));
bytes[36..40].copy_from_slice(&u32::to_le_bytes(*state.text_editor_state.current_styles.line_height.state() as u32));
bytes[40..44].copy_from_slice(&u32::to_le_bytes(*state.text_editor_state.current_styles.letter_spacing.state() as u32));
bytes[44..48].copy_from_slice(&usize::to_le_bytes(state.text_editor_state.current_styles.fills.len()));
// vertical_align
bytes[0..4].copy_from_slice(&u32::to_le_bytes(styles.vertical_align as u32));
let offset: usize = 48;
for fill in state.text_editor_state.current_styles.fills.iter() {
// text_align: state + value
bytes[4..8].copy_from_slice(&u32::to_le_bytes(*styles.text_align.state() as u32));
if let Some(val) = styles.text_align.value() {
bytes[8..12].copy_from_slice(&u32::to_le_bytes(text_align_to_raw(val)));
}
std::ptr::null_mut()
// text_direction: state + value
bytes[12..16].copy_from_slice(&u32::to_le_bytes(*styles.text_direction.state() as u32));
if let Some(val) = styles.text_direction.value() {
bytes[16..20].copy_from_slice(&u32::to_le_bytes(text_direction_to_raw(val)));
}
// text_decoration: state + value
bytes[20..24].copy_from_slice(&u32::to_le_bytes(*styles.text_decoration.state() as u32));
// text_decoration merges Option<TextDecoration>, so value() is Option<Option<TextDecoration>>
// When Single: value() is Some(inner) where inner is the Option<TextDecoration>
if styles.text_decoration.is_single() {
let inner = styles.text_decoration.value();
// inner is &Option<TextDecoration>: the Some/None tells us Single vs not,
// but for text_decoration the merged value IS an Option<TextDecoration>.
// So we serialize the inner Option directly.
bytes[24..28].copy_from_slice(&u32::to_le_bytes(text_decoration_to_raw(inner)));
}
// text_transform: state + value (same pattern as text_decoration)
bytes[28..32].copy_from_slice(&u32::to_le_bytes(*styles.text_transform.state() as u32));
if styles.text_transform.is_single() {
let inner = styles.text_transform.value();
bytes[32..36].copy_from_slice(&u32::to_le_bytes(text_transform_to_raw(inner)));
}
// font_size: state + value (f32)
bytes[36..40].copy_from_slice(&u32::to_le_bytes(*styles.font_size.state() as u32));
if let Some(val) = styles.font_size.value() {
bytes[40..44].copy_from_slice(&f32::to_le_bytes(*val));
}
// font_weight: state + value (i32)
bytes[44..48].copy_from_slice(&u32::to_le_bytes(*styles.font_weight.state() as u32));
if let Some(val) = styles.font_weight.value() {
bytes[48..52].copy_from_slice(&i32::to_le_bytes(*val));
}
// line_height: state + value (f32)
bytes[52..56].copy_from_slice(&u32::to_le_bytes(*styles.line_height.state() as u32));
if let Some(val) = styles.line_height.value() {
bytes[56..60].copy_from_slice(&f32::to_le_bytes(*val));
}
// letter_spacing: state + value (f32)
bytes[60..64].copy_from_slice(&u32::to_le_bytes(*styles.letter_spacing.state() as u32));
if let Some(val) = styles.letter_spacing.value() {
bytes[64..68].copy_from_slice(&f32::to_le_bytes(*val));
}
// font_family: state + id (16 bytes) + style (u32) + weight (u32)
bytes[68..72].copy_from_slice(&u32::to_le_bytes(*styles.font_family.state() as u32));
if let Some(family) = styles.font_family.value() {
let id_bytes: [u8; 16] = family.id().into();
bytes[72..88].copy_from_slice(&id_bytes);
bytes[88..92].copy_from_slice(&u32::to_le_bytes(font_style_to_raw(&family.style())));
bytes[92..96].copy_from_slice(&u32::to_le_bytes(family.weight()));
}
// font_variant_id: state + uuid (16 bytes)
bytes[96..100].copy_from_slice(&u32::to_le_bytes(*styles.font_variant_id.state() as u32));
if let Some(variant_id) = styles.font_variant_id.value() {
let id_bytes: [u8; 16] = (*variant_id).into();
bytes[100..116].copy_from_slice(&id_bytes);
}
// fills_count + fill data
bytes[116..120].copy_from_slice(&u32::to_le_bytes(styles.fills.len() as u32));
if !fill_bytes.is_empty() {
bytes[STYLES_HEADER_SIZE..].copy_from_slice(&fill_bytes);
}
mem::write_bytes(bytes)
})
}