Mayo commited on
fix: bold/italic cause postcard unable reopen
Browse files- koharu-app/src/session.rs +67 -1
- koharu-core/src/style.rs +58 -7
koharu-app/src/session.rs
CHANGED
|
@@ -229,7 +229,9 @@ struct ProjectTomlFile {
|
|
| 229 |
mod tests {
|
| 230 |
use super::*;
|
| 231 |
use camino::Utf8PathBuf;
|
| 232 |
-
use koharu_core::{
|
|
|
|
|
|
|
| 233 |
use tempfile::tempdir;
|
| 234 |
|
| 235 |
fn tmp_dir() -> (tempfile::TempDir, Utf8PathBuf) {
|
|
@@ -257,6 +259,70 @@ mod tests {
|
|
| 257 |
assert!(session.scene.read().pages.contains_key(&page_id));
|
| 258 |
}
|
| 259 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
#[test]
|
| 261 |
fn exclusive_lock_prevents_second_open() {
|
| 262 |
let (_tmp, path) = tmp_dir();
|
|
|
|
| 229 |
mod tests {
|
| 230 |
use super::*;
|
| 231 |
use camino::Utf8PathBuf;
|
| 232 |
+
use koharu_core::{
|
| 233 |
+
Node, NodeId, NodeKind, Op, Page, PageId, TextData, TextShaderEffect, TextStyle, Transform,
|
| 234 |
+
};
|
| 235 |
use tempfile::tempdir;
|
| 236 |
|
| 237 |
fn tmp_dir() -> (tempfile::TempDir, Utf8PathBuf) {
|
|
|
|
| 259 |
assert!(session.scene.read().pages.contains_key(&page_id));
|
| 260 |
}
|
| 261 |
|
| 262 |
+
#[test]
|
| 263 |
+
fn reopen_preserves_text_style_effects_in_scene_bin() {
|
| 264 |
+
let (_tmp, path) = tmp_dir();
|
| 265 |
+
let page_id: PageId;
|
| 266 |
+
let node_id: NodeId;
|
| 267 |
+
{
|
| 268 |
+
let session = ProjectSession::create(&path, "styled").unwrap();
|
| 269 |
+
let page = Page::new("p1", 800, 600);
|
| 270 |
+
page_id = page.id;
|
| 271 |
+
session
|
| 272 |
+
.apply(Op::AddPage { page, at: 0 })
|
| 273 |
+
.expect("apply AddPage");
|
| 274 |
+
|
| 275 |
+
node_id = NodeId::new();
|
| 276 |
+
let mut scene = session.scene.write();
|
| 277 |
+
let page = scene.pages.get_mut(&page_id).expect("page");
|
| 278 |
+
page.nodes.insert(
|
| 279 |
+
node_id,
|
| 280 |
+
Node {
|
| 281 |
+
id: node_id,
|
| 282 |
+
transform: Transform {
|
| 283 |
+
x: 0.0,
|
| 284 |
+
y: 0.0,
|
| 285 |
+
width: 100.0,
|
| 286 |
+
height: 40.0,
|
| 287 |
+
rotation_deg: 0.0,
|
| 288 |
+
},
|
| 289 |
+
visible: true,
|
| 290 |
+
kind: NodeKind::Text(TextData {
|
| 291 |
+
style: Some(TextStyle {
|
| 292 |
+
font_families: vec!["Arial".to_string()],
|
| 293 |
+
font_size: Some(20.0),
|
| 294 |
+
color: [0, 0, 0, 255],
|
| 295 |
+
effect: Some(TextShaderEffect {
|
| 296 |
+
italic: true,
|
| 297 |
+
bold: true,
|
| 298 |
+
}),
|
| 299 |
+
stroke: None,
|
| 300 |
+
text_align: None,
|
| 301 |
+
}),
|
| 302 |
+
..Default::default()
|
| 303 |
+
}),
|
| 304 |
+
},
|
| 305 |
+
);
|
| 306 |
+
drop(scene);
|
| 307 |
+
session.compact().unwrap();
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
let session = ProjectSession::open(&path).unwrap();
|
| 311 |
+
let scene = session.scene.read();
|
| 312 |
+
let page = scene.pages.get(&page_id).expect("page");
|
| 313 |
+
let node = page.nodes.get(&node_id).expect("node");
|
| 314 |
+
let NodeKind::Text(text) = &node.kind else {
|
| 315 |
+
panic!("expected text node");
|
| 316 |
+
};
|
| 317 |
+
let effect = text
|
| 318 |
+
.style
|
| 319 |
+
.as_ref()
|
| 320 |
+
.and_then(|style| style.effect)
|
| 321 |
+
.expect("effect");
|
| 322 |
+
assert!(effect.italic);
|
| 323 |
+
assert!(effect.bold);
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
#[test]
|
| 327 |
fn exclusive_lock_prevents_second_open() {
|
| 328 |
let (_tmp, path) = tmp_dir();
|
koharu-core/src/style.rs
CHANGED
|
@@ -151,6 +151,13 @@ impl<'de> Deserialize<'de> for TextShaderEffect {
|
|
| 151 |
bold: Option<bool>,
|
| 152 |
}
|
| 153 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
#[derive(Deserialize)]
|
| 155 |
#[serde(untagged)]
|
| 156 |
enum Repr {
|
|
@@ -158,13 +165,18 @@ impl<'de> Deserialize<'de> for TextShaderEffect {
|
|
| 158 |
Legacy(String),
|
| 159 |
}
|
| 160 |
|
| 161 |
-
|
| 162 |
-
Repr::
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
|
|
|
|
|
|
| 167 |
}
|
|
|
|
|
|
|
|
|
|
| 168 |
}
|
| 169 |
}
|
| 170 |
|
|
@@ -219,7 +231,7 @@ pub struct TextStyle {
|
|
| 219 |
|
| 220 |
#[cfg(test)]
|
| 221 |
mod tests {
|
| 222 |
-
use super::TextShaderEffect;
|
| 223 |
|
| 224 |
#[test]
|
| 225 |
fn parse_combined_effects() {
|
|
@@ -240,4 +252,43 @@ mod tests {
|
|
| 240 |
let effect: TextShaderEffect = "none".parse().expect("parse");
|
| 241 |
assert_eq!(effect.to_string(), "none");
|
| 242 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
}
|
|
|
|
| 151 |
bold: Option<bool>,
|
| 152 |
}
|
| 153 |
|
| 154 |
+
#[derive(Deserialize)]
|
| 155 |
+
#[serde(deny_unknown_fields)]
|
| 156 |
+
struct BinaryFlagsRepr {
|
| 157 |
+
italic: bool,
|
| 158 |
+
bold: bool,
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
#[derive(Deserialize)]
|
| 162 |
#[serde(untagged)]
|
| 163 |
enum Repr {
|
|
|
|
| 165 |
Legacy(String),
|
| 166 |
}
|
| 167 |
|
| 168 |
+
if deserializer.is_human_readable() {
|
| 169 |
+
return match Repr::deserialize(deserializer)? {
|
| 170 |
+
Repr::Flags(FlagsRepr { italic, bold }) => Ok(Self {
|
| 171 |
+
italic: italic.unwrap_or(false),
|
| 172 |
+
bold: bold.unwrap_or(false),
|
| 173 |
+
}),
|
| 174 |
+
Repr::Legacy(value) => value.parse().map_err(serde::de::Error::custom),
|
| 175 |
+
};
|
| 176 |
}
|
| 177 |
+
|
| 178 |
+
let BinaryFlagsRepr { italic, bold } = BinaryFlagsRepr::deserialize(deserializer)?;
|
| 179 |
+
Ok(Self { italic, bold })
|
| 180 |
}
|
| 181 |
}
|
| 182 |
|
|
|
|
| 231 |
|
| 232 |
#[cfg(test)]
|
| 233 |
mod tests {
|
| 234 |
+
use super::{TextShaderEffect, TextStyle};
|
| 235 |
|
| 236 |
#[test]
|
| 237 |
fn parse_combined_effects() {
|
|
|
|
| 252 |
let effect: TextShaderEffect = "none".parse().expect("parse");
|
| 253 |
assert_eq!(effect.to_string(), "none");
|
| 254 |
}
|
| 255 |
+
|
| 256 |
+
#[test]
|
| 257 |
+
fn json_legacy_string_deserializes() {
|
| 258 |
+
let effect: TextShaderEffect = serde_json::from_str("\"italic,bold\"").expect("json");
|
| 259 |
+
assert!(effect.italic);
|
| 260 |
+
assert!(effect.bold);
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
#[test]
|
| 264 |
+
fn postcard_text_shader_effect_round_trips() {
|
| 265 |
+
let effect = TextShaderEffect {
|
| 266 |
+
italic: true,
|
| 267 |
+
bold: true,
|
| 268 |
+
};
|
| 269 |
+
let bytes = postcard::to_allocvec(&effect).expect("serialize");
|
| 270 |
+
let decoded: TextShaderEffect = postcard::from_bytes(&bytes).expect("deserialize");
|
| 271 |
+
assert!(decoded.italic);
|
| 272 |
+
assert!(decoded.bold);
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
#[test]
|
| 276 |
+
fn postcard_text_style_with_effect_round_trips() {
|
| 277 |
+
let style = TextStyle {
|
| 278 |
+
font_families: vec!["Arial".to_string()],
|
| 279 |
+
font_size: Some(18.0),
|
| 280 |
+
color: [12, 34, 56, 255],
|
| 281 |
+
effect: Some(TextShaderEffect {
|
| 282 |
+
italic: true,
|
| 283 |
+
bold: false,
|
| 284 |
+
}),
|
| 285 |
+
stroke: None,
|
| 286 |
+
text_align: None,
|
| 287 |
+
};
|
| 288 |
+
let bytes = postcard::to_allocvec(&style).expect("serialize");
|
| 289 |
+
let decoded: TextStyle = postcard::from_bytes(&bytes).expect("deserialize");
|
| 290 |
+
let effect = decoded.effect.expect("effect");
|
| 291 |
+
assert!(effect.italic);
|
| 292 |
+
assert!(!effect.bold);
|
| 293 |
+
}
|
| 294 |
}
|