Files
vectorcraft/crates/ui-egui/src/panels/tests_css_properties.rs
T
bflatasticandClaude Opus 5.5 ed4eceb837 M4.96: Window › CSS Properties shows, copies and exports the CSS of the selection
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>
2026-10-05 16:03:11 +07:00

104 lines
4.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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"));
}