mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-02 22:20:50 -06:00
The svg crate's new CSS writer (css_rules) gives the CSS web pages style objects with: rectangles and ellipses as background-color / linear-/radial-gradient(), border and border-radius; type as font, color, spacing and alignment; shadows, glows and blur as box-shadow, text-shadow and filter. It shares SVG export's declarations (transparency, fonts, type spacing, colours, gradient stops), which moved into svg/src/css.rs. Options: units, absolute position, width and height, unnamed objects, rasterize unsupported art (as a PNG background-image). Commands css.selection, css.generate and css.export; UI commands css.copy and css.exportFile; the CSS Properties panel with a highlighted code view. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
104 lines
4.2 KiB
Rust
104 lines
4.2 KiB
Rust
//! The CSS Properties panel drawn headless: the selection's CSS in the code view, Generate CSS
|
||
//! for the whole document, the export options in the panel menu, and Copy Selected Style and
|
||
//! Export through their UI commands.
|
||
|
||
use std::cell::RefCell;
|
||
use std::rc::Rc;
|
||
|
||
use serde_json::json;
|
||
use vectorcraft_engine::Session;
|
||
use vectorcraft_svg::{CssOptions, CssUnits};
|
||
|
||
use super::css_properties::{ALL, ID, OPTIONS, menu, show};
|
||
use super::set_pstate;
|
||
use crate::tests_labels::painted_text;
|
||
use crate::{Services, VectorcraftApp};
|
||
|
||
type Log = Rc<RefCell<Vec<(String, Vec<u8>)>>>;
|
||
|
||
/// An app with a selected red 30 × 20 rectangle; files written go to the log.
|
||
fn app() -> (VectorcraftApp, Log) {
|
||
let log = Log::default();
|
||
let l = log.clone();
|
||
let services = Services {
|
||
write: Some(Box::new(move |path: &str, bytes: &[u8]| {
|
||
l.borrow_mut().push((path.to_string(), bytes.to_vec()));
|
||
Ok(())
|
||
})),
|
||
..Services::default()
|
||
};
|
||
let mut app = VectorcraftApp::new(Session::new(), services);
|
||
app.run("file.new", json!({"width": 200, "height": 100})).unwrap();
|
||
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 30, "height": 20})).unwrap();
|
||
app.run("paint.setFill", json!({"color": "#ff0000"})).unwrap();
|
||
(app, log)
|
||
}
|
||
|
||
#[test]
|
||
fn the_code_view_shows_the_selections_css_with_the_options() {
|
||
let (mut app, _) = app();
|
||
assert!(crate::state::ICON_PANELS.iter().any(|(id, ..)| *id == ID));
|
||
let text = painted_text(&mut app, show);
|
||
assert!(text.contains(".rectangle {") && text.contains("background-color: #ff0000;") && text.contains("width: 30px;"), "{text}");
|
||
let text = painted_text(&mut app, |app, ui| {
|
||
set_pstate(ui.ctx(), OPTIONS, CssOptions { units: CssUnits::Pt, position: true, ..CssOptions::default() });
|
||
show(app, ui);
|
||
});
|
||
assert!(text.contains("width: 30pt;") && text.contains("left: 10pt;"), "{text}");
|
||
}
|
||
|
||
#[test]
|
||
fn generate_css_shows_the_document_while_nothing_is_selected() {
|
||
let (mut app, _) = app();
|
||
app.run("shape.ellipse", json!({"x": 100, "y": 10, "width": 20, "height": 20})).unwrap();
|
||
app.run("select.none", json!({})).unwrap();
|
||
let text = painted_text(&mut app, show);
|
||
assert!(text.contains("No selection") && !text.contains(".rectangle"), "{text}");
|
||
let text = painted_text(&mut app, |app, ui| {
|
||
set_pstate(ui.ctx(), ALL, true);
|
||
show(app, ui);
|
||
});
|
||
assert!(text.contains(".rectangle {") && text.contains(".ellipse {") && text.contains("border-radius: 50%;"), "{text}");
|
||
}
|
||
|
||
#[test]
|
||
fn the_panel_menu_lists_the_actions_and_options() {
|
||
let (mut app, _) = app();
|
||
let text = painted_text(&mut app, menu);
|
||
for item in [
|
||
"Copy Selected Style",
|
||
"Export Selected CSS…",
|
||
"Export All…",
|
||
"Generate CSS",
|
||
"Units: px",
|
||
"Absolute Position",
|
||
"Width and Height",
|
||
"Unnamed Objects",
|
||
"Rasterize Unsupported Art",
|
||
] {
|
||
assert!(text.contains(item), "{item} in {text}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn copy_and_export_go_through_the_ui_commands() {
|
||
let (mut app, log) = app();
|
||
let r = app.run("css.copy", json!({})).unwrap();
|
||
assert_eq!(r["rules"], json!(1));
|
||
assert!(app.clipboard_out.as_deref().is_some_and(|c| c.starts_with(".rectangle {") && c.contains("#ff0000")));
|
||
app.run("select.none", json!({})).unwrap();
|
||
assert!(app.run("css.copy", json!({})).is_err(), "nothing selected: nothing to copy");
|
||
// Export All with a star rasterized: the sheet and the star's picture beside it.
|
||
app.run("shape.star", json!({"cx": 100, "cy": 50, "radius1": 30, "radius2": 15})).unwrap();
|
||
let r = app.run("css.exportFile", json!({"path": "/tmp/site/site.css", "scope": "all", "rasterize": true})).unwrap();
|
||
assert_eq!(r["rules"], json!(2));
|
||
let png = std::path::Path::new("/tmp/site").join("path.png").to_string_lossy().into_owned();
|
||
assert_eq!(r["images"], json!([png]));
|
||
let log = log.borrow();
|
||
let names: Vec<&str> = log.iter().map(|(p, _)| p.as_str()).collect();
|
||
assert_eq!(names, ["/tmp/site/site.css", png.as_str()]);
|
||
let css = String::from_utf8(log[0].1.clone()).unwrap();
|
||
assert!(css.contains(".rectangle {") && css.contains("background-image: url(path.png);"), "{css}");
|
||
assert!(log[1].1.starts_with(b"\x89PNG"));
|
||
}
|