diff --git a/.github/workflows/test-crates.yml b/.github/workflows/test-crates.yml index 956b3a27b0..e120686381 100644 --- a/.github/workflows/test-crates.yml +++ b/.github/workflows/test-crates.yml @@ -31,6 +31,63 @@ jobs: - name: Check formatting run: cargo fmt --all -- --check + clippy: + name: cargo clippy + needs: [test] + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Free up disk space + run: | + sudo rm -rf /usr/share/dotnet + sudo rm -rf /usr/local/lib/android + sudo rm -rf /opt/ghc + sudo rm -rf /opt/hostedtoolcache/CodeQL + sudo docker image prune --all --force + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + libexpat1-dev \ + libfontconfig1-dev \ + libfreetype6-dev \ + libgl1-mesa-dev \ + libgles2-mesa-dev \ + libwayland-dev \ + libx11-dev \ + libx11-xcb-dev \ + libxcb-render0-dev \ + libxcb-shape0-dev \ + libxcb-xfixes0-dev \ + libxcursor-dev \ + libxi-dev \ + libxinerama-dev \ + libxrandr-dev \ + libxxf86vm-dev \ + mesa-common-dev + + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + toolchain: 1.92.0 + components: clippy + + - name: Cache cargo build outputs + uses: Swatinem/rust-cache@v2 + with: + shared-key: test-native + cache-all-crates: true + cache-workspace-crates: true + cache-on-failure: true + + - name: Run clippy + env: + FORCE_SKIA_BINARIES_DOWNLOAD: "1" + run: cargo clippy --no-deps --workspace --exclude grida-canvas-wasm -- -D warnings + test: name: cargo test runs-on: ubuntu-latest diff --git a/Cargo.toml b/Cargo.toml index afebbdab57..a50469fd50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,10 @@ members = [ "crates/math2", ] +[workspace.lints.clippy] +# Style choices for a graphics engine — intentionally allowed +upper_case_acronyms = "allow" # AABB, RGB, HSL, SVG are domain terms + # Reduce debug information for tests to lower memory usage during linking in CI # opt-level = 1 speeds up compilation while maintaining debuggability [profile.test] diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 0000000000..3675d449a1 --- /dev/null +++ b/clippy.toml @@ -0,0 +1,5 @@ +# Clippy configuration for the Grida workspace +# https://doc.rust-lang.org/clippy/lint_configuration.html + +# Graphics/rendering functions legitimately accept many parameters +too-many-arguments-threshold = 12 diff --git a/crates/csscascade/Cargo.toml b/crates/csscascade/Cargo.toml index 8ba63aa174..9c302b393d 100644 --- a/crates/csscascade/Cargo.toml +++ b/crates/csscascade/Cargo.toml @@ -35,3 +35,6 @@ markup5ever = "0.36.1" tendril = "0.4.3" atomic_refcell = "0.1.13" +[lints] +workspace = true + diff --git a/crates/csscascade/src/adapter.rs b/crates/csscascade/src/adapter.rs index 24eea2b34b..2402465638 100644 --- a/crates/csscascade/src/adapter.rs +++ b/crates/csscascade/src/adapter.rs @@ -191,7 +191,7 @@ impl HtmlElement { self.attr_iter() .filter(|(attr, _)| namespace_matches(ns, &attr.name.ns)) .find(|(_, stored)| *stored == local_name) - .map_or(false, |(attr, _)| operation.eval_str(attr.value.as_ref())) + .is_some_and(|(attr, _)| operation.eval_str(attr.value.as_ref())) } fn lang_attribute_value(&self) -> Option<&str> { @@ -815,7 +815,7 @@ fn namespace_matches( } fn atom_ident_str(atom: &AtomIdent) -> &str { - atom.as_ref().as_ref() + atom.as_ref() } fn sibling_pair(id: NodeId) -> (Option, Option) { diff --git a/crates/csscascade/src/dom.rs b/crates/csscascade/src/dom.rs index 22f979bce5..d84f2217bd 100644 --- a/crates/csscascade/src/dom.rs +++ b/crates/csscascade/src/dom.rs @@ -207,8 +207,7 @@ impl ElemNameTrait for OwnedElemName { impl DemoDomBuilder { fn new() -> Self { - let mut nodes = Vec::new(); - nodes.push(NodeTemp::new(NodeDataTemp::Document)); + let nodes = vec![NodeTemp::new(NodeDataTemp::Document)]; Self { nodes: RefCell::new(nodes), document: NodeId(0), @@ -317,7 +316,7 @@ impl DemoDomBuilder { let mut existing = existing.borrow_mut(); let existing_names: Vec<_> = existing.iter().map(|attr| attr.name.clone()).collect(); for attr in attrs { - if existing_names.iter().any(|name| *name == attr.name) { + if existing_names.contains(&attr.name) { continue; } existing.push(attr); @@ -468,12 +467,11 @@ impl TreeSink for DemoDomBuilder { } fn append(&self, parent: &Self::Handle, child: NodeOrText) { - if let NodeOrText::AppendText(ref text) = child { - if let Some(last) = self.last_child(*parent) { - if self.append_to_existing_text(last, text) { - return; - } - } + if let NodeOrText::AppendText(ref text) = child + && let Some(last) = self.last_child(*parent) + && self.append_to_existing_text(last, text) + { + return; } let new_child = match child { diff --git a/crates/csscascade/src/rcdom/mod.rs b/crates/csscascade/src/rcdom/mod.rs index f05114e82e..e0bf50fcd6 100644 --- a/crates/csscascade/src/rcdom/mod.rs +++ b/crates/csscascade/src/rcdom/mod.rs @@ -140,10 +140,9 @@ impl Drop for Node { ref template_contents, .. } = node.data + && let Some(template_contents) = template_contents.borrow_mut().take() { - if let Some(template_contents) = template_contents.borrow_mut().take() { - nodes.push(template_contents); - } + nodes.push(template_contents); } } } @@ -300,12 +299,11 @@ impl TreeSink for RcDom { fn append(&self, parent: &Handle, child: NodeOrText) { // Append to an existing Text node if we have one. - if let NodeOrText::AppendText(text) = &child { - if let Some(h) = parent.children.borrow().last() { - if append_to_existing_text(h, text) { - return; - } - } + if let NodeOrText::AppendText(text) = &child + && let Some(h) = parent.children.borrow().last() + && append_to_existing_text(h, text) + { + return; } append( diff --git a/crates/csscascade/src/tree/mod.rs b/crates/csscascade/src/tree/mod.rs index 03655014ef..14e5abe104 100644 --- a/crates/csscascade/src/tree/mod.rs +++ b/crates/csscascade/src/tree/mod.rs @@ -74,6 +74,7 @@ pub struct Tree { impl Tree { /// Creates a tree from the supplied root node. + #[allow(clippy::arc_with_non_send_sync)] fn new(root: StyledNode, runtime: Arc) -> Self { Self { root: Arc::new(root), @@ -89,6 +90,7 @@ impl Tree { /// /// NOTE: The current implementation only mirrors the DOM structure with stub /// styles; full cascade integration will replace this once the Stylo bridge is wired. + #[allow(clippy::should_implement_trait)] pub fn from_str(input: &str) -> Result { let runtime = StyleRuntime::new()?; let opts = default_parse_opts(); @@ -125,11 +127,13 @@ impl Tree { let handle = styled_node_to_dom(&self.root, options); let serializable: SerializableHandle = handle.into(); let mut buffer = Vec::new(); - let mut serialize_opts = SerializeOpts::default(); - serialize_opts.traversal_scope = if options.include_root() { - TraversalScope::IncludeNode - } else { - TraversalScope::ChildrenOnly(None) + let serialize_opts = SerializeOpts { + traversal_scope: if options.include_root() { + TraversalScope::IncludeNode + } else { + TraversalScope::ChildrenOnly(None) + }, + ..Default::default() }; serialize(&mut buffer, &serializable, serialize_opts).map_err(TreeError::Serialize)?; String::from_utf8(buffer).map_err(TreeError::Utf8) @@ -194,7 +198,7 @@ impl StyledNode { } /// A thin, type-safe wrapper around node identifiers. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] pub struct NodeId(u64); impl NodeId { @@ -203,12 +207,6 @@ impl NodeId { } } -impl Default for NodeId { - fn default() -> Self { - Self(0) - } -} - /// Describes what kind of content a node holds. #[derive(Debug, Clone, PartialEq, Eq)] pub enum NodeKind { @@ -422,14 +420,14 @@ fn styled_node_to_dom(node: &StyledNode, options: &WriteOptions) -> Handle { }) .collect(); - if options.inline_styles() { - if let Some(serialized) = serialize_all_properties(&node.get_style()) { - attrs_vec.retain(|attr| attr.name.local.as_ref() != "style"); - attrs_vec.push(HtmlAttribute { - name: QualName::new(None, ns!(), LocalName::from("style")), - value: StrTendril::from(serialized.as_str()), - }); - } + if options.inline_styles() + && let Some(serialized) = serialize_all_properties(&node.get_style()) + { + attrs_vec.retain(|attr| attr.name.local.as_ref() != "style"); + attrs_vec.push(HtmlAttribute { + name: QualName::new(None, ns!(), LocalName::from("style")), + value: StrTendril::from(serialized.as_str()), + }); } let handle = Node::new(NodeData::Element { name: qual, @@ -517,6 +515,7 @@ impl StyleRuntime { let stylist = Stylist::new(device, quirks_mode); + #[allow(clippy::arc_with_non_send_sync)] Ok(Arc::new(Self { stylist, shared_lock, diff --git a/crates/grida-canvas-fonts/Cargo.toml b/crates/grida-canvas-fonts/Cargo.toml index 0855100b67..3f7236b691 100644 --- a/crates/grida-canvas-fonts/Cargo.toml +++ b/crates/grida-canvas-fonts/Cargo.toml @@ -31,3 +31,6 @@ serde_json = "1.0" [features] default = [] serde = ["dep:serde"] + +[lints] +workspace = true diff --git a/crates/grida-canvas-fonts/src/parse.rs b/crates/grida-canvas-fonts/src/parse.rs index 5a1ab7f9cd..a899bb2461 100644 --- a/crates/grida-canvas-fonts/src/parse.rs +++ b/crates/grida-canvas-fonts/src/parse.rs @@ -372,7 +372,7 @@ fn parse_stat(face: &Face<'_>, data: &[u8]) -> StatData { let mut axes: Vec = Vec::new(); let mut tags: Vec = Vec::new(); - for record in table.axes.clone() { + for record in table.axes { let tag = tag_to_string(&record.tag.to_bytes()); let name = lookup_name(face, record.name_id).unwrap_or_default(); tags.push(tag.clone()); diff --git a/crates/grida-canvas-fonts/src/parse_feature/mode_full.rs b/crates/grida-canvas-fonts/src/parse_feature/mode_full.rs index b882640569..58cfd4e6a4 100644 --- a/crates/grida-canvas-fonts/src/parse_feature/mode_full.rs +++ b/crates/grida-canvas-fonts/src/parse_feature/mode_full.rs @@ -15,6 +15,12 @@ pub struct ComprehensiveFeatureParser { // No state needed for this parser } +impl Default for ComprehensiveFeatureParser { + fn default() -> Self { + Self::new() + } +} + impl ComprehensiveFeatureParser { pub fn new() -> Self { Self {} @@ -128,8 +134,8 @@ fn parse_gpos_features_with_context( // Check if any scripts have languages let mut has_language_systems = false; for i in 0..gpos_table.scripts.len() { - if let Some(script) = gpos_table.scripts.get(i as u16) { - if script.languages.len() > 0 { + if let Some(script) = gpos_table.scripts.get(i) { + if !script.languages.is_empty() { has_language_systems = true; break; } @@ -139,7 +145,7 @@ fn parse_gpos_features_with_context( if has_language_systems { // Parse features through script/language hierarchy (like GSUB) for i in 0..gpos_table.scripts.len() { - if let Some(script) = gpos_table.scripts.get(i as u16) { + if let Some(script) = gpos_table.scripts.get(i) { let script_tag = script.tag.to_string(); for language in script.languages { @@ -184,7 +190,7 @@ fn parse_gpos_features_with_context( } else { // No language systems - parse features directly (like Inter font) for i in 0..gpos_table.features.len() { - if let Some(feature) = gpos_table.features.get(i as u16) { + if let Some(feature) = gpos_table.features.get(i) { let tag = feature.tag.to_string(); let lookup_indices: Vec = feature.lookup_indices.into_iter().collect(); @@ -334,7 +340,7 @@ fn analyze_gpos_feature( /// This function processes OpenType coverage tables to extract glyph IDs /// and convert them to Unicode characters for feature analysis. fn extract_coverage_glyphs(coverage: &Coverage, glyph_set: &mut HashSet) { - let glyph_ids = coverage_glyphs(coverage.clone()); + let glyph_ids = coverage_glyphs(*coverage); for glyph_id in glyph_ids { glyph_set.insert(glyph_id.to_string()); } diff --git a/crates/grida-canvas-fonts/src/parse_feature/mode_simple.rs b/crates/grida-canvas-fonts/src/parse_feature/mode_simple.rs index 3062de5fc7..409c932a17 100644 --- a/crates/grida-canvas-fonts/src/parse_feature/mode_simple.rs +++ b/crates/grida-canvas-fonts/src/parse_feature/mode_simple.rs @@ -12,6 +12,12 @@ pub struct BuiltinFeatureParser { // No state needed for this parser } +impl Default for BuiltinFeatureParser { + fn default() -> Self { + Self::new() + } +} + impl BuiltinFeatureParser { pub fn new() -> Self { Self {} @@ -70,7 +76,7 @@ fn parse_gsub_features_builtin( // Iterate through features directly (similar to our chained approach) for i in 0..gsub_table.features.len() { - if let Some(feature) = gsub_table.features.get(i as u16) { + if let Some(feature) = gsub_table.features.get(i) { let tag = feature.tag.to_string(); let name = utils::get_feature_name_from_name_table( face, @@ -172,7 +178,7 @@ fn parse_gpos_features_builtin( // Iterate through features directly for i in 0..gpos_table.features.len() { - if let Some(feature) = gpos_table.features.get(i as u16) { + if let Some(feature) = gpos_table.features.get(i) { let tag = feature.tag.to_string(); let name = utils::get_feature_name_from_name_table( face, diff --git a/crates/grida-canvas-fonts/src/parse_feature_params.rs b/crates/grida-canvas-fonts/src/parse_feature_params.rs index 4153163cf7..d051def30f 100644 --- a/crates/grida-canvas-fonts/src/parse_feature_params.rs +++ b/crates/grida-canvas-fonts/src/parse_feature_params.rs @@ -247,7 +247,7 @@ fn lookup_name_by_id(font_data: &[u8], name_id: u16) -> Option { /// Decodes UTF-16 BE string data fn decode_utf16_be(data: &[u8]) -> Option { - if data.len() % 2 != 0 { + if !data.len().is_multiple_of(2) { return None; } diff --git a/crates/grida-canvas-fonts/src/parse_ui.rs b/crates/grida-canvas-fonts/src/parse_ui.rs index 21056ee567..ed8847088a 100644 --- a/crates/grida-canvas-fonts/src/parse_ui.rs +++ b/crates/grida-canvas-fonts/src/parse_ui.rs @@ -172,7 +172,7 @@ impl UIFontParser { let mut face_records = Vec::new(); for font_face in &font_faces { - let parser = Parser::new(&font_face.data) + let parser = Parser::new(font_face.data) .map_err(|e| format!("Failed to parse font '{}': {}", font_face.face_id, e))?; let face_record = parser @@ -325,12 +325,10 @@ impl UIFontParser { } else { format!("{} {} Italic", weight_name, stretch_name) } + } else if weight_key == 400 && stretch_key == 5 { + "Regular".to_string() } else { - if weight_key == 400 && stretch_key == 5 { - "Regular".to_string() - } else { - format!("{} {}", weight_name, stretch_name) - } + format!("{} {}", weight_name, stretch_name) } } @@ -418,7 +416,7 @@ impl UIFontParser { for parser in parsers { if parser.is_variable() { let fvar_data = parser.fvar(); - for (_, axis) in &fvar_data.axes { + for axis in fvar_data.axes.values() { let entry = axis_map.entry(axis.tag.clone()).or_insert(( axis.name.clone(), axis.min, @@ -454,7 +452,7 @@ impl UIFontParser { ) -> Result, String> { let mut face_info = Vec::new(); - for (_index, (parser, face_record)) in parsers.iter().zip(face_records.iter()).enumerate() { + for (parser, face_record) in parsers.iter().zip(face_records.iter()) { let features = parser.ffeatures(); // Get face-specific axes and instances if this is a variable font @@ -1219,7 +1217,7 @@ impl UIFontParser { vf_recipe .axis_values .get("ital") - .map_or(true, |&val| val == 0.0) + .is_none_or(|&val| val == 0.0) } else { // No variable recipe - check the is_italic field !recipe.is_italic @@ -1260,16 +1258,16 @@ impl UIFontParser { // Calculate distances and create matches let mut matches: Vec = roman_capabilities .into_iter() - .filter_map(|recipe| { + .map(|recipe| { // Calculate distance between current style and this roman recipe let (distance, axis_diffs) = self.calculate_style_distance(¤t_style, &recipe, &family_result.axes); - Some(ItalicMatch { + ItalicMatch { recipe, distance, axis_diffs, - }) + } }) .collect(); diff --git a/crates/grida-canvas-fonts/src/selection.rs b/crates/grida-canvas-fonts/src/selection.rs index c96657405c..790b9674aa 100644 --- a/crates/grida-canvas-fonts/src/selection.rs +++ b/crates/grida-canvas-fonts/src/selection.rs @@ -523,7 +523,7 @@ impl FontSelectionParser { pub fn has_italic_named_instances(&self, face: &FaceRecord) -> bool { // For Scenario 3-1, we MUST have italic-named instances in the name table // This is strict - we only check the main name table entries - self.extract_italic_instances(face).len() > 0 + !self.extract_italic_instances(face).is_empty() } /// Extracts italic instances from the name table for Scenario 3-1 fonts. diff --git a/crates/grida-canvas-fonts/src/selection_italic.rs b/crates/grida-canvas-fonts/src/selection_italic.rs index bb997d2572..2caab03f73 100644 --- a/crates/grida-canvas-fonts/src/selection_italic.rs +++ b/crates/grida-canvas-fonts/src/selection_italic.rs @@ -146,7 +146,7 @@ impl Default for ItalicSelectionParser { /// /// These types maintain compatibility with existing code while using the new /// Selection terminology internally. - +/// /// Legacy alias for FontStyle to maintain compatibility. pub type ItalicKind = FontStyle; diff --git a/crates/grida-canvas-wasm/Cargo.toml b/crates/grida-canvas-wasm/Cargo.toml index 0ab9cf246d..32e23dd10d 100644 --- a/crates/grida-canvas-wasm/Cargo.toml +++ b/crates/grida-canvas-wasm/Cargo.toml @@ -14,3 +14,6 @@ serde_json = "1.0" [features] default = [] + +[lints] +workspace = true diff --git a/crates/grida-canvas-wasm/src/wasm_application.rs b/crates/grida-canvas-wasm/src/wasm_application.rs index 937334eca7..2aa45ec4a2 100644 --- a/crates/grida-canvas-wasm/src/wasm_application.rs +++ b/crates/grida-canvas-wasm/src/wasm_application.rs @@ -9,7 +9,7 @@ fn alloc_len_prefixed(bytes: &[u8]) -> *const u8 { return std::ptr::null(); }; let total = 4 + bytes.len(); - let out = allocate(total) as *mut u8; + let out = allocate(total); let len_bytes = len_u32.to_le_bytes(); unsafe { std::ptr::copy_nonoverlapping(len_bytes.as_ptr(), out, 4); @@ -713,7 +713,7 @@ pub unsafe extern "C" fn get_image_bytes( if let (Some(app), Some(id)) = (app.as_mut(), __str_from_ptr_len(id_ptr, id_len)) { if let Some(bytes) = app.get_image_bytes(&id) { let len = bytes.len(); - let out = allocate(len + 4) as *mut u8; + let out = allocate(len + 4); let len_bytes = (len as u32).to_le_bytes(); std::ptr::copy_nonoverlapping(len_bytes.as_ptr(), out, 4); std::ptr::copy_nonoverlapping(bytes.as_ptr(), out.add(4), len); @@ -994,7 +994,7 @@ pub unsafe extern "C" fn export_node_as( // Allocate memory for: [4 bytes for length] + [actual data] let total_size = 4 + data_len; - let out = allocate(total_size) as *mut u8; + let out = allocate(total_size); // Write the length as first 4 bytes (little-endian u32) let len_bytes = (data_len as u32).to_le_bytes(); diff --git a/crates/grida-canvas/Cargo.toml b/crates/grida-canvas/Cargo.toml index ed43ef1abe..cddbc42658 100644 --- a/crates/grida-canvas/Cargo.toml +++ b/crates/grida-canvas/Cargo.toml @@ -188,3 +188,6 @@ required-features = ["native-gl-context"] [[example]] name = "tool_io_grida" +[lints] +workspace = true + diff --git a/crates/grida-canvas/src/cache/atlas/mod.rs b/crates/grida-canvas/src/cache/atlas/mod.rs index da3f8ffe1b..e5139a7132 100644 --- a/crates/grida-canvas/src/cache/atlas/mod.rs +++ b/crates/grida-canvas/src/cache/atlas/mod.rs @@ -10,6 +10,7 @@ //! - [`atlas`] — Single atlas page: GPU surface + packer + slot-to-node mapping. //! - [`atlas_set`] — Manages multiple atlas pages with overflow and eviction. +#[allow(clippy::module_inception)] pub mod atlas; pub mod atlas_set; pub mod packing; diff --git a/crates/grida-canvas/src/cache/compositor/promotion.rs b/crates/grida-canvas/src/cache/compositor/promotion.rs index e16d1a7a87..bc096aafa2 100644 --- a/crates/grida-canvas/src/cache/compositor/promotion.rs +++ b/crates/grida-canvas/src/cache/compositor/promotion.rs @@ -47,8 +47,8 @@ const MIN_SCREEN_AREA: f32 = 4.0 * 4.0; /// promoted. Measured on M2 Pro with 400 visible simple rects: /// - live draw: 3.2ms (400 rect fills) /// - cached blit: 4.1ms + 0.4ms gpu_flush (400 texture blits) -/// So for simple geometry, live draw wins. Effects change the -/// equation because a single shadow node can cost 100µs+ to paint. +/// So for simple geometry, live draw wins. Effects change the +/// equation because a single shadow node can cost 100µs+ to paint. pub fn should_promote( layer: &PainterPictureLayer, _render_bounds: &Rectangle, diff --git a/crates/grida-canvas/src/cache/fast_hash.rs b/crates/grida-canvas/src/cache/fast_hash.rs index d68c1061c1..13761a390a 100644 --- a/crates/grida-canvas/src/cache/fast_hash.rs +++ b/crates/grida-canvas/src/cache/fast_hash.rs @@ -34,7 +34,7 @@ impl Hasher for NodeIdHasher { fn write_u64(&mut self, i: u64) { // FxHash: XOR-fold then multiply by a large odd constant. // This is the primary fast path for NodeId (u64) keys. - self.hash = self.hash ^ i; + self.hash ^= i; self.hash = self.hash.wrapping_mul(0x517cc1b727220a95); } diff --git a/crates/grida-canvas/src/cache/geometry.rs b/crates/grida-canvas/src/cache/geometry.rs index 7fd0ec0607..946f78e37e 100644 --- a/crates/grida-canvas/src/cache/geometry.rs +++ b/crates/grida-canvas/src/cache/geometry.rs @@ -87,6 +87,12 @@ pub struct GeometryCache { entries: DenseNodeMap, } +impl Default for GeometryCache { + fn default() -> Self { + Self::new() + } +} + impl GeometryCache { pub fn new() -> Self { Self { @@ -534,6 +540,10 @@ impl GeometryCache { self.entries.len() } + pub fn is_empty(&self) -> bool { + self.entries.is_empty() + } + pub fn has(&self, id: &NodeId) -> bool { self.entries.contains_key(id) } diff --git a/crates/grida-canvas/src/cache/paragraph.rs b/crates/grida-canvas/src/cache/paragraph.rs index 46b6cac19d..3e210a5f11 100644 --- a/crates/grida-canvas/src/cache/paragraph.rs +++ b/crates/grida-canvas/src/cache/paragraph.rs @@ -327,8 +327,7 @@ impl ParagraphCache { // Store in the appropriate cache if let Some(node_id) = id { - self.entries_measurement_by_id - .insert(node_id.clone(), entry); + self.entries_measurement_by_id.insert(*node_id, entry); } else if let Some(hash) = shape_key { self.entries_measurement_by_shapekey_unstable .insert(hash, entry); @@ -576,4 +575,8 @@ impl ParagraphCache { pub fn len(&self) -> usize { self.entries_measurement_by_id.len() + self.entries_measurement_by_shapekey_unstable.len() } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } } diff --git a/crates/grida-canvas/src/cache/picture.rs b/crates/grida-canvas/src/cache/picture.rs index f6800f69db..b61ec233a4 100644 --- a/crates/grida-canvas/src/cache/picture.rs +++ b/crates/grida-canvas/src/cache/picture.rs @@ -7,17 +7,11 @@ use skia_safe::Picture; /// Currently only the `depth` parameter is used: /// - `None` caches the entire scene as a single picture. /// - `Some(depth)` caches up to `depth` levels of nodes separately. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct PictureCacheStrategy { pub depth: Option, } -impl Default for PictureCacheStrategy { - fn default() -> Self { - Self { depth: None } - } -} - #[derive(Debug, Clone)] pub struct PictureCache { strategy: PictureCacheStrategy, @@ -31,6 +25,12 @@ pub struct PictureCache { generation: u64, } +impl Default for PictureCache { + fn default() -> Self { + Self::new() + } +} + impl PictureCache { pub fn new() -> Self { Self { @@ -74,7 +74,7 @@ impl PictureCache { if variant_key == 0 { return self.default_store.get(id); } - self.variant_store.get(&(id.clone(), variant_key)) + self.variant_store.get(&(*id, variant_key)) } /// Store a picture for a node in a specific render variant. @@ -93,6 +93,10 @@ impl PictureCache { self.default_store.len() + self.variant_store.len() } + pub fn is_empty(&self) -> bool { + self.default_store.is_empty() && self.variant_store.is_empty() + } + /// Returns true when the variant store has no entries. /// When this is true AND variant unification is enabled, ALL cached /// pictures live under the default key (0), making the prefill skip diff --git a/crates/grida-canvas/src/cache/scene.rs b/crates/grida-canvas/src/cache/scene.rs index e603017600..ef8f251b8b 100644 --- a/crates/grida-canvas/src/cache/scene.rs +++ b/crates/grida-canvas/src/cache/scene.rs @@ -45,6 +45,12 @@ pub struct SceneCache { pub layer_index: RTree, } +impl Default for SceneCache { + fn default() -> Self { + Self::new() + } +} + impl SceneCache { /// Create a new empty cache with the given picture cache strategy. pub fn new() -> Self { diff --git a/crates/grida-canvas/src/cache/vector_path.rs b/crates/grida-canvas/src/cache/vector_path.rs index ff32b989c7..4d9e0c9976 100644 --- a/crates/grida-canvas/src/cache/vector_path.rs +++ b/crates/grida-canvas/src/cache/vector_path.rs @@ -39,7 +39,7 @@ impl VectorPathCache { let path = skia_safe::path::Path::from_svg(data).expect("invalid SVG path"); let rc = Rc::new(path); self.entries.insert( - id.clone(), + *id, VectorPathCacheEntry { hash, path: rc.clone(), @@ -56,6 +56,10 @@ impl VectorPathCache { self.entries.len() } + pub fn is_empty(&self) -> bool { + self.entries.is_empty() + } + pub fn get(&self, id: &NodeId) -> Option<&VectorPathCacheEntry> { self.entries.get(id) } diff --git a/crates/grida-canvas/src/cg/colormatrix.rs b/crates/grida-canvas/src/cg/colormatrix.rs index 95788f3e8b..e8ef20aacc 100644 --- a/crates/grida-canvas/src/cg/colormatrix.rs +++ b/crates/grida-canvas/src/cg/colormatrix.rs @@ -35,7 +35,7 @@ pub fn saturation(s: f32) -> [f32; 20] { 0.0, 0.0, 0.0, 1.0, 0.0, ]; - return matrix; + matrix } /// Generate a 5x4 color matrix for hue rotation (SVG/CSS-compatible). @@ -92,7 +92,7 @@ pub fn hue_rotate(angle: f32) -> [f32; 20] { 0.0, 0.0, 0.0, 1.0, 0.0, ]; - return matrix; + matrix } #[rustfmt::skip] diff --git a/crates/grida-canvas/src/cg/fe.rs b/crates/grida-canvas/src/cg/fe.rs index 4a98b5b07f..4312f39447 100644 --- a/crates/grida-canvas/src/cg/fe.rs +++ b/crates/grida-canvas/src/cg/fe.rs @@ -59,9 +59,9 @@ impl FilterShadowEffect { } } -impl Into for FilterShadowEffect { - fn into(self) -> FilterEffect { - match self { +impl From for FilterEffect { + fn from(val: FilterShadowEffect) -> Self { + match val { FilterShadowEffect::DropShadow(shadow) => FilterEffect::DropShadow(shadow), FilterShadowEffect::InnerShadow(shadow) => FilterEffect::InnerShadow(shadow), } diff --git a/crates/grida-canvas/src/cg/stroke_dasharray.rs b/crates/grida-canvas/src/cg/stroke_dasharray.rs index 05acde205d..ac94826e83 100644 --- a/crates/grida-canvas/src/cg/stroke_dasharray.rs +++ b/crates/grida-canvas/src/cg/stroke_dasharray.rs @@ -77,7 +77,6 @@ use serde::{Deserialize, Serialize}; /// - [Skia SkPathEffect](https://skia.org/docs/user/api/skpaint_overview/#patheffect) #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(transparent)] - pub struct StrokeDashArray(pub Vec); impl StrokeDashArray { diff --git a/crates/grida-canvas/src/cg/stroke_width.rs b/crates/grida-canvas/src/cg/stroke_width.rs index 28789417b2..b529098fee 100644 --- a/crates/grida-canvas/src/cg/stroke_width.rs +++ b/crates/grida-canvas/src/cg/stroke_width.rs @@ -93,9 +93,10 @@ /// } /// } /// ``` -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub enum StrokeWidth { /// No stroke (all widths are 0) + #[default] None, /// Uniform stroke width for all sides Uniform(f32), @@ -164,12 +165,6 @@ impl From for StrokeWidth { } } -impl Default for StrokeWidth { - fn default() -> Self { - StrokeWidth::None - } -} - /// Universal input format for stroke width values (CSS-like). /// /// This is the storage/serialization format that serves as the universal input @@ -261,7 +256,7 @@ impl UnknownStrokeWidth { } // (3) - return None; + None } } diff --git a/crates/grida-canvas/src/cg/svg.rs b/crates/grida-canvas/src/cg/svg.rs index f7cf3fa668..333510766b 100644 --- a/crates/grida-canvas/src/cg/svg.rs +++ b/crates/grida-canvas/src/cg/svg.rs @@ -296,9 +296,9 @@ fn scale(sx: f32, sy: f32) -> AffineTransform { AffineTransform::from_acebdf(sx, 0.0, 0.0, 0.0, sy, 0.0) } -/// SVG Packed Scene is dedicated struct for archive / transport format of resolved SVG file. -/// rules: -/// - size efficient: table-like structure similar to ttf +// SVG Packed Scene is dedicated struct for archive / transport format of resolved SVG file. +// rules: +// - size efficient: table-like structure similar to ttf // pub struct SVGPackedScene { // images // paints @@ -306,7 +306,6 @@ fn scale(sx: f32, sy: f32) -> AffineTransform { // } /// Intermediate Representation of an SVG node. - #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "kind")] pub enum IRSVGChildNode { @@ -357,6 +356,7 @@ pub struct IRSVGTextNode { /// A positioned text chunk — either uniform (single style) or attributed /// (per-span style variation). +#[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, Serialize, Deserialize)] pub enum IRSVGTextChunk { /// Single-style chunk → packs to `TextSpanNode`. diff --git a/crates/grida-canvas/src/cg/tilemode.rs b/crates/grida-canvas/src/cg/tilemode.rs index aa5c24174e..dc4e367a20 100644 --- a/crates/grida-canvas/src/cg/tilemode.rs +++ b/crates/grida-canvas/src/cg/tilemode.rs @@ -70,9 +70,10 @@ use serde::{Deserialize, Serialize}; /// # Default /// /// The default mode is [`Clamp`], matching Skia, Flutter, and SVG defaults. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)] pub enum TileMode { #[serde(rename = "clamp")] + #[default] Clamp, #[serde(rename = "repeated", alias = "repeat")] Repeated, @@ -81,9 +82,3 @@ pub enum TileMode { #[serde(rename = "decal")] Decal, } - -impl Default for TileMode { - fn default() -> Self { - TileMode::Clamp - } -} diff --git a/crates/grida-canvas/src/cg/transform.rs b/crates/grida-canvas/src/cg/transform.rs index 0957597fc0..89ab08436e 100644 --- a/crates/grida-canvas/src/cg/transform.rs +++ b/crates/grida-canvas/src/cg/transform.rs @@ -8,7 +8,6 @@ use serde::{Deserialize, Serialize}; /// ``` #[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[serde(into = "[[f32; 3]; 2]", from = "[[f32; 3]; 2]")] - pub struct CGTransform2D { pub m00: f32, pub m01: f32, diff --git a/crates/grida-canvas/src/cg/types.rs b/crates/grida-canvas/src/cg/types.rs index 03856c1a79..8d2f4477e7 100644 --- a/crates/grida-canvas/src/cg/types.rs +++ b/crates/grida-canvas/src/cg/types.rs @@ -46,9 +46,9 @@ impl Default for CGPoint { } } -impl Into for CGPoint { - fn into(self) -> skia_safe::Point { - skia_safe::Point::new(self.x, self.y) +impl From for skia_safe::Point { + fn from(val: CGPoint) -> Self { + skia_safe::Point::new(val.x, val.y) } } @@ -84,13 +84,14 @@ impl Default for LayerMaskType { } } -#[derive(Debug, Clone, Copy, Deserialize, PartialEq)] +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Default)] pub enum ImageMaskType { /// Alpha channel masking. /// /// Uses the alpha channel of the mask to determine the opacity of the masked content. /// Areas with higher alpha values in the mask will show the content more opaquely. #[serde(rename = "alpha")] + #[default] Alpha, /// Luminance-based masking. /// @@ -100,12 +101,6 @@ pub enum ImageMaskType { Luminance, } -impl Default for ImageMaskType { - fn default() -> Self { - ImageMaskType::Alpha - } -} - /// Boolean path operation. #[derive(Debug, Clone, Copy, Deserialize, PartialEq)] pub enum BooleanPathOperation { @@ -208,9 +203,11 @@ pub type ContainerClipFlag = bool; /// - `Blend(BlendMode::Normal)` ≈ `isolation: isolate` + normal compositing #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)] #[serde(untagged)] +#[derive(Default)] pub enum LayerBlendMode { /// Non-isolated group/layer; children/paints blend directly with the backdrop. #[serde(rename = "pass-through")] + #[default] PassThrough, /// Isolated layer composited with a specific blend mode. Blend(BlendMode), @@ -223,31 +220,26 @@ impl From for LayerBlendMode { } } -impl Into for LayerBlendMode { - fn into(self) -> BlendMode { - match self { +impl From for BlendMode { + fn from(val: LayerBlendMode) -> Self { + match val { LayerBlendMode::PassThrough => BlendMode::Normal, LayerBlendMode::Blend(mode) => mode, } } } -impl Default for LayerBlendMode { - fn default() -> Self { - LayerBlendMode::PassThrough - } -} - /// Blend functions for compositing paints or isolated layers (does **not** include PassThrough). /// /// - SVG: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/mix-blend-mode /// - Skia: https://skia.org/docs/user/api/SkBlendMode_Reference/ /// - Flutter: https://api.flutter.dev/flutter/dart-ui/BlendMode.html /// - Figma: https://help.figma.com/hc/en-us/articles/360039956994 -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)] pub enum BlendMode { // Skia: kSrcOver, CSS: normal #[serde(rename = "normal")] + #[default] Normal, // Skia: kMultiply #[serde(rename = "multiply")] @@ -296,26 +288,15 @@ pub enum BlendMode { Luminosity, } -impl Default for BlendMode { - fn default() -> Self { - BlendMode::Normal - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)] pub enum FillRule { #[serde(rename = "nonzero")] + #[default] NonZero, #[serde(rename = "evenodd")] EvenOdd, } -impl Default for FillRule { - fn default() -> Self { - FillRule::NonZero - } -} - /// Defines the shape of stroke endpoints (line caps). /// /// `StrokeCap` determines how the ends of open paths are rendered when stroked. @@ -392,10 +373,11 @@ impl Default for FillRule { /// /// - [`StrokeAlign`] - Controls stroke positioning relative to path /// - [`StrokeDashArray`] - Defines dash patterns for strokes -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)] pub enum StrokeCap { /// Flat edge perpendicular to the stroke direction (default) #[serde(rename = "butt", alias = "none")] + #[default] Butt, /// Semicircular cap extending beyond the endpoint #[serde(rename = "round")] @@ -405,12 +387,6 @@ pub enum StrokeCap { Square, } -impl Default for StrokeCap { - fn default() -> Self { - StrokeCap::Butt - } -} - /// Built-in marker presets placed at stroke endpoints or vector vertices. /// /// Unlike [`StrokeCap`] (which maps to native backend caps like Skia `PaintCap`), @@ -565,10 +541,11 @@ impl StrokeMarkerPreset { /// /// - [`StrokeCap`] - Controls stroke endpoints for open paths /// - [`StrokeAlign`] - Controls stroke positioning relative to path -#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)] pub enum StrokeJoin { /// Sharp pointed corner with miter limit fallback (default) #[serde(rename = "miter")] + #[default] Miter, /// Circular arc connecting path segments #[serde(rename = "round")] @@ -578,12 +555,6 @@ pub enum StrokeJoin { Bevel, } -impl Default for StrokeJoin { - fn default() -> Self { - StrokeJoin::Miter - } -} - /// Miter limit for stroke joins. /// /// Controls when a miter join falls back to a bevel join based on the ratio @@ -669,9 +640,10 @@ impl From for StrokeMiterLimit { /// /// - [Flutter](https://api.flutter.dev/flutter/painting/BorderSide/strokeAlign.html) /// - [Figma](https://www.figma.com/plugin-docs/api/properties/nodes-strokealign/) -#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)] pub enum StrokeAlign { #[serde(rename = "inside")] + #[default] Inside, #[serde(rename = "center")] Center, @@ -679,37 +651,27 @@ pub enum StrokeAlign { Outside, } -impl Default for StrokeAlign { - fn default() -> Self { - StrokeAlign::Inside - } -} - /// Represents a single axis in 2D space. /// - [Flutter](https://api.flutter.dev/flutter/painting/Axis.html) -#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Default)] pub enum Axis { #[serde(rename = "horizontal")] + #[default] Horizontal, #[serde(rename = "vertical")] Vertical, } -impl Default for Axis { - fn default() -> Self { - Axis::Horizontal - } -} - /// Alignment of items along the main axis. /// /// See also: /// - [MDN justify-content](https://developer.mozilla.org/en-US/docs/Web/CSS/justify-content) /// - [MDN Main Axis](https://developer.mozilla.org/en-US/docs/Glossary/Main_Axis) /// - [Flutter MainAxisAlignment](https://api.flutter.dev/flutter/rendering/MainAxisAlignment.html) -#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Default)] pub enum MainAxisAlignment { #[serde(rename = "start")] + #[default] Start, #[serde(rename = "end")] End, @@ -725,21 +687,16 @@ pub enum MainAxisAlignment { Stretch, } -impl Default for MainAxisAlignment { - fn default() -> Self { - MainAxisAlignment::Start - } -} - /// Alignment of items along the cross axis. /// /// See also: /// - [MDN align-items](https://developer.mozilla.org/en-US/docs/Web/CSS/align-items) /// - [MDN Cross Axis](https://developer.mozilla.org/en-US/docs/Glossary/Cross_Axis) /// - [Flutter CrossAxisAlignment](https://api.flutter.dev/flutter/rendering/CrossAxisAlignment.html) -#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Default)] pub enum CrossAxisAlignment { #[serde(rename = "start")] + #[default] Start, #[serde(rename = "end")] End, @@ -749,12 +706,6 @@ pub enum CrossAxisAlignment { Stretch, } -impl Default for CrossAxisAlignment { - fn default() -> Self { - CrossAxisAlignment::Start - } -} - /// Represents **inset distances from the edges** of a rectangular box. /// /// `EdgeInsets` defines per-edge padding or margin values around a box. @@ -857,30 +808,20 @@ impl Default for EdgeInsets { } } -#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Default)] pub enum LayoutMode { + #[default] Normal, Flex, } -impl Default for LayoutMode { - fn default() -> Self { - LayoutMode::Normal - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Default)] pub enum LayoutPositioning { + #[default] Auto, Absolute, } -impl Default for LayoutPositioning { - fn default() -> Self { - LayoutPositioning::Auto - } -} - /// Constraint positioning specifier for constraints layout. /// /// Defines how a node is anchored along an axis (horizontal or vertical) relative to its parent container. @@ -896,9 +837,10 @@ impl Default for LayoutPositioning { /// - [`End`](LayoutConstraintAnchor::End): Anchored to the bottom edge /// - [`Center`](LayoutConstraintAnchor::Center): Centered vertically /// - [`Stretch`](LayoutConstraintAnchor::Stretch): Anchored to both top and bottom edges -#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Default)] pub enum LayoutConstraintAnchor { /// Start anchor (left for horizontal, top for vertical) + #[default] Start, /// End anchor (right for horizontal, bottom for vertical) End, @@ -908,12 +850,6 @@ pub enum LayoutConstraintAnchor { Stretch, } -impl Default for LayoutConstraintAnchor { - fn default() -> Self { - LayoutConstraintAnchor::Start - } -} - /// Defines how a node is constrained relative to its parent container. /// /// Specifies the constraint positioning behavior for both horizontal and vertical axes, @@ -1022,22 +958,17 @@ impl Default for LayoutConstraints { /// * [Taffy FlexWrap](https://docs.rs/taffy/latest/taffy/style/enum.FlexWrap.html) /// * [`LayoutGap`] — spacing between items and lines /// * [`Axis`] — main axis direction -#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Default)] pub enum LayoutWrap { /// Items wrap onto multiple lines as needed #[serde(rename = "wrap")] Wrap, /// All items are forced into a single line #[serde(rename = "nowrap")] + #[default] NoWrap, } -impl Default for LayoutWrap { - fn default() -> Self { - LayoutWrap::NoWrap - } -} - /// Represents the **spacing between adjacent elements** in a flex layout container. /// /// `LayoutGap` is a 2-dimensional scalar type describing the distance between @@ -1200,18 +1131,18 @@ impl Radius { } } -impl Into for Radius { - fn into(self) -> CGPoint { +impl From for CGPoint { + fn from(val: Radius) -> Self { CGPoint { - x: self.rx, - y: self.ry, + x: val.rx, + y: val.ry, } } } -impl Into<(f32, f32)> for Radius { - fn into(self) -> (f32, f32) { - (self.rx, self.ry) +impl From for (f32, f32) { + fn from(val: Radius) -> Self { + (val.rx, val.ry) } } @@ -1316,9 +1247,10 @@ impl Default for CornerSmoothing { /// Text Transform (Text Case) /// - [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/text-transform) -#[derive(Debug, Clone, Copy, Serialize, Deserialize, Hash, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, Hash, PartialEq, Eq, Default)] pub enum TextTransform { #[serde(rename = "none")] + #[default] None, #[serde(rename = "uppercase")] Uppercase, @@ -1328,21 +1260,16 @@ pub enum TextTransform { Capitalize, } -impl Default for TextTransform { - fn default() -> Self { - TextTransform::None - } -} - /// Supported text decoration modes. /// /// Only `Underline` and `None` are supported in the current version. /// /// - [Flutter](https://api.flutter.dev/flutter/dart-ui/TextDecoration-class.html) /// - [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/text-decoration-line) -#[derive(Debug, Clone, Copy, Serialize, Deserialize, Hash, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, Hash, PartialEq, Eq, Default)] pub enum TextDecorationLine { #[serde(rename = "none")] + #[default] None, #[serde(rename = "underline")] Underline, @@ -1352,15 +1279,10 @@ pub enum TextDecorationLine { LineThrough, } -impl Default for TextDecorationLine { - fn default() -> Self { - TextDecorationLine::None - } -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, Hash, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, Hash, PartialEq, Eq, Default)] pub enum TextDecorationStyle { #[serde(rename = "solid")] + #[default] Solid, #[serde(rename = "double")] Double, @@ -1372,12 +1294,6 @@ pub enum TextDecorationStyle { Wavy, } -impl Default for TextDecorationStyle { - fn default() -> Self { - TextDecorationStyle::Solid - } -} - pub trait FromWithContext { fn from_with_context(value: T, ctx: &C) -> Self; } @@ -1472,18 +1388,16 @@ impl Default for TextDecoration { impl FromWithContext for TextDecoration { fn from_with_context(value: TextDecorationRec, ctx: &DecorationRecBuildContext) -> Self { let text_decoration_color = value.text_decoration_color.unwrap_or(ctx.color); - let text_decoration_style = value - .text_decoration_style - .unwrap_or(TextDecorationStyle::default()); + let text_decoration_style = value.text_decoration_style.unwrap_or_default(); let text_decoration_skip_ink = value.text_decoration_skip_ink.unwrap_or(true); let text_decoration_thickness = value.text_decoration_thickness.unwrap_or(1.0); Self { text_decoration_line: value.text_decoration_line, - text_decoration_color: text_decoration_color, - text_decoration_style: text_decoration_style, - text_decoration_skip_ink: text_decoration_skip_ink, - text_decoration_thickness: text_decoration_thickness, + text_decoration_color, + text_decoration_style, + text_decoration_skip_ink, + text_decoration_thickness, } } } @@ -1494,9 +1408,10 @@ impl FromWithContext for TextDecor /// /// - [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/text-align) /// - [Flutter](https://api.flutter.dev/flutter/dart-ui/TextAlign.html) -#[derive(Debug, Clone, Copy, Serialize, Deserialize, Hash, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, Hash, PartialEq, Eq, Default)] pub enum TextAlign { #[serde(rename = "left")] + #[default] Left, #[serde(rename = "right")] Right, @@ -1506,12 +1421,6 @@ pub enum TextAlign { Justify, } -impl Default for TextAlign { - fn default() -> Self { - TextAlign::Left - } -} - /// Supported vertical alignment values for text within its container height. /// /// This enum defines how text is positioned vertically within the height container @@ -1577,7 +1486,7 @@ impl Default for TextAlign { /// /// This approach allows for flexible text positioning while maintaining compatibility /// with Skia's text layout engine limitations. -#[derive(Debug, Clone, Copy, Serialize, Deserialize, Hash, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, Hash, PartialEq, Eq, Default)] pub enum TextAlignVertical { /// Align text to the top of the container. /// @@ -1585,6 +1494,7 @@ pub enum TextAlignVertical { /// When the container height is smaller than the text height, /// the bottom portion of the text will be clipped. #[serde(rename = "top")] + #[default] Top, /// Center text vertically within the container. @@ -1604,12 +1514,6 @@ pub enum TextAlignVertical { Bottom, } -impl Default for TextAlignVertical { - fn default() -> Self { - TextAlignVertical::Top - } -} - /// Font weight value (1-1000). /// /// - [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight) @@ -1636,7 +1540,7 @@ impl FontWeight { /// Panics if the value is not between 1 and 1000. pub fn new(value: u32) -> Self { assert!( - value >= 1 && value <= 1000, + (1..=1000).contains(&value), "Font weight must be between 1 and 1000" ); Self(value) @@ -1680,24 +1584,20 @@ pub struct FontVariation { pub value: f32, } -#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)] pub enum FontOpticalSizing { /// Auto mode will set the optical size to the font size. /// this is the default behavior. + #[default] Auto, None, Fixed(f32), } -impl Default for FontOpticalSizing { - fn default() -> Self { - FontOpticalSizing::Auto - } -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] pub enum TextLineHeight { /// Normal (unset, no override) + #[default] Normal, /// px value Fixed(f32), @@ -1705,12 +1605,6 @@ pub enum TextLineHeight { Factor(f32), } -impl Default for TextLineHeight { - fn default() -> Self { - TextLineHeight::Normal - } -} - #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub enum TextLetterSpacing { /// Fixed value in px. @@ -2159,7 +2053,7 @@ impl Paint { if self.opacity() == 0.0 { return false; } - return true; + true } pub fn blend_mode(&self) -> BlendMode { @@ -2991,7 +2885,7 @@ pub struct ImageTile { /// See also: /// - https://developer.mozilla.org/en-US/docs/Web/CSS/background-repeat /// - https://api.flutter.dev/flutter/painting/ImageRepeat.html -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] pub enum ImageRepeat { /// Repeat the image horizontally (X axis) only. #[serde(rename = "repeat-x")] @@ -3001,15 +2895,10 @@ pub enum ImageRepeat { RepeatY, /// Repeat the image in both directions. #[serde(rename = "repeat")] + #[default] Repeat, } -impl Default for ImageRepeat { - fn default() -> Self { - ImageRepeat::Repeat - } -} - /// Defines how an image should be painted within its container. /// /// `ImagePaint` combines an image resource with fitting behavior, visual properties, diff --git a/crates/grida-canvas/src/devtools/ruler_overlay.rs b/crates/grida-canvas/src/devtools/ruler_overlay.rs index 1a1d04848f..b9ee74a94f 100644 --- a/crates/grida-canvas/src/devtools/ruler_overlay.rs +++ b/crates/grida-canvas/src/devtools/ruler_overlay.rs @@ -32,7 +32,7 @@ thread_local! { static FONT: Font = Font::new(crate::fonts::embedded::typeface(crate::fonts::embedded::geistmono::BYTES), 10.0); - static CACHE: RefCell> = RefCell::new(None); + static CACHE: RefCell> = const { RefCell::new(None) }; } impl Ruler { diff --git a/crates/grida-canvas/src/export/export_as_image.rs b/crates/grida-canvas/src/export/export_as_image.rs index bda12d90a5..5efa0f0227 100644 --- a/crates/grida-canvas/src/export/export_as_image.rs +++ b/crates/grida-canvas/src/export/export_as_image.rs @@ -12,9 +12,9 @@ use crate::{ use math2::Rectangle; use skia_safe::EncodedImageFormat; -impl Into for ExportAsImage { - fn into(self) -> EncodedImageFormat { - match self { +impl From for EncodedImageFormat { + fn from(val: ExportAsImage) -> Self { + match val { ExportAsImage::PNG(_) => EncodedImageFormat::PNG, ExportAsImage::JPEG(_) => EncodedImageFormat::JPEG, ExportAsImage::WEBP(_) => EncodedImageFormat::WEBP, @@ -81,17 +81,14 @@ pub fn export_node_as_image( _ => None, }; - let Some(data) = image.encode(None, skfmt, quality) else { - return None; - }; + let data = image.encode(None, skfmt, quality)?; // Return the exported data - let exported = match format { + + match format { ExportAsImage::PNG(_) => Some(Exported::PNG(data.to_vec())), ExportAsImage::JPEG(_) => Some(Exported::JPEG(data.to_vec())), ExportAsImage::WEBP(_) => Some(Exported::WEBP(data.to_vec())), ExportAsImage::BMP(_) => Some(Exported::BMP(data.to_vec())), - }; - - exported + } } diff --git a/crates/grida-canvas/src/export/mod.rs b/crates/grida-canvas/src/export/mod.rs index 3b23a8b29e..2163ddbc78 100644 --- a/crates/grida-canvas/src/export/mod.rs +++ b/crates/grida-canvas/src/export/mod.rs @@ -64,7 +64,7 @@ impl ExportSize { height: self.height * scale, }, ExportConstraints::ScaleToWidth(width) => { - let target_w = *width as f32; + let target_w = *width; let target_h = if self.width > 0.0 { self.height * target_w / self.width } else { @@ -76,7 +76,7 @@ impl ExportSize { } } ExportConstraints::ScaleToHeight(height) => { - let target_h = *height as f32; + let target_h = *height; let target_w = if self.height > 0.0 { self.width * target_h / self.height } else { @@ -103,9 +103,7 @@ pub fn export_node_as( // 1. find node // get the size of the node - let Some(rect) = geometry.get_render_bounds(node_id) else { - return None; - }; + let rect = geometry.get_render_bounds(node_id)?; let width = rect.width; let height = rect.height; @@ -117,17 +115,17 @@ pub fn export_node_as( ExportAs::PDF(pdf_format) => pdf_format, _ => unreachable!(), }; - return export_node_as_pdf(scene, fonts, images, rect, format); + export_node_as_pdf(scene, fonts, images, rect, format) } else if format.is_format_svg() { let format: ExportAsSVG = match format { ExportAs::SVG(svg_format) => svg_format, _ => unreachable!(), }; - return export_node_as_svg(scene, fonts, images, rect, format); + export_node_as_svg(scene, fonts, images, rect, format) } else if format.is_format_image() { let format: ExportAsImage = format.clone().try_into().unwrap(); - return export_node_as_image(scene, fonts, images, size, rect, format); + export_node_as_image(scene, fonts, images, size, rect, format) } else { - return None; + None } } diff --git a/crates/grida-canvas/src/hittest/hit_tester.rs b/crates/grida-canvas/src/hittest/hit_tester.rs index 7e6520e6ad..a0c201c9d1 100644 --- a/crates/grida-canvas/src/hittest/hit_tester.rs +++ b/crates/grida-canvas/src/hittest/hit_tester.rs @@ -85,22 +85,15 @@ impl<'a> HitTester<'a> { while let Some(id) = current_id { // Get the node to check if it's a container with clip enabled - if let Ok(node) = graph.get_node(&id) { - match node { - Node::Container(n) => { - if n.clip { - // Check if the point is within this container's bounds - if let Some(bounds) = self.cache.geometry.get_world_bounds(&id) { - if !rect::contains_point(&bounds, point) { - // Point is outside this clipping container's bounds - return false; - } - } + if let Ok(Node::Container(n)) = graph.get_node(&id) { + if n.clip { + // Check if the point is within this container's bounds + if let Some(bounds) = self.cache.geometry.get_world_bounds(&id) { + if !rect::contains_point(&bounds, point) { + // Point is outside this clipping container's bounds + return false; } } - _ => { - // Other node types don't have clipping - } } } @@ -314,11 +307,11 @@ impl<'a> HitTester<'a> { // Bounds + clip check (same as `intersects`) if let Some(bounds) = self.cache.geometry.get_world_bounds(id) { - if rect::intersects(&bounds, rect) { - if self.is_point_within_parent_clip_bounds(id, center_point) { - selected_set.insert(*id); - out.push(*id); - } + if rect::intersects(&bounds, rect) + && self.is_point_within_parent_clip_bounds(id, center_point) + { + selected_set.insert(*id); + out.push(*id); } } } diff --git a/crates/grida-canvas/src/html/mod.rs b/crates/grida-canvas/src/html/mod.rs index cb851c9641..128491a066 100644 --- a/crates/grida-canvas/src/html/mod.rs +++ b/crates/grida-canvas/src/html/mod.rs @@ -40,7 +40,7 @@ use style::values::specified::text::TextDecorationLine as StyloTextDecorationLin /// Callers must serialize access externally (e.g. via a mutex). pub fn from_html_str(html: &str) -> Result { // Ensure Stylo thread state is initialized (idempotent after first call). - let _ = thread_state::initialize(ThreadState::LAYOUT); + thread_state::initialize(ThreadState::LAYOUT); // 1. Parse HTML into arena DOM let dom = @@ -344,10 +344,7 @@ impl SceneBuilder { } else { // No visual properties — safe to merge margin into padding. // This avoids an extra wrapper node in the tree. - let existing = node - .layout_container - .layout_padding - .unwrap_or(EdgeInsets::zero()); + let existing = node.layout_container.layout_padding.unwrap_or_default(); node.layout_container.layout_padding = Some(EdgeInsets { top: existing.top + margin.top, right: existing.right + margin.right, @@ -869,8 +866,8 @@ fn css_background_to_fills(style: &ComputedValues) -> Paints { // 2. Background images (gradient layers on top) for image in bg.background_image.0.iter() { - match image { - GenericImage::Gradient(gradient) => match gradient.as_ref() { + if let GenericImage::Gradient(gradient) = image { + match gradient.as_ref() { GenericGradient::Linear { direction, items, .. } => { @@ -907,8 +904,7 @@ fn css_background_to_fills(style: &ComputedValues) -> Paints { ..Default::default() })); } - }, - _ => {} + } } } @@ -985,6 +981,7 @@ fn gradient_items_to_stops( let start_offset = raw[start].0.unwrap(); let end_offset = raw[end].0.unwrap(); let count = (end - start) as f32; + #[allow(clippy::needless_range_loop)] for j in (start + 1)..end { let t = (j - start) as f32 / count; raw[j].0 = Some(start_offset + t * (end_offset - start_offset)); @@ -1057,6 +1054,7 @@ fn conic_gradient_items_to_stops( let start_offset = raw[start].0.unwrap(); let end_offset = raw[end].0.unwrap(); let count = (end - start) as f32; + #[allow(clippy::needless_range_loop)] for j in (start + 1)..end { let t = (j - start) as f32 / count; raw[j].0 = Some(start_offset + t * (end_offset - start_offset)); @@ -1339,49 +1337,37 @@ fn css_dimensions_to_cg(style: &ComputedValues, dims: &mut LayoutDimensionStyle) } // min-width - match &pos.min_width { - GenericSize::LengthPercentage(lp) => { - if let Some(len) = lp.0.to_length() { - let px = len.px(); - if px > 0.0 { - dims.layout_min_width = Some(px); - } + if let GenericSize::LengthPercentage(lp) = &pos.min_width { + if let Some(len) = lp.0.to_length() { + let px = len.px(); + if px > 0.0 { + dims.layout_min_width = Some(px); } } - _ => {} } // min-height - match &pos.min_height { - GenericSize::LengthPercentage(lp) => { - if let Some(len) = lp.0.to_length() { - let px = len.px(); - if px > 0.0 { - dims.layout_min_height = Some(px); - } + if let GenericSize::LengthPercentage(lp) = &pos.min_height { + if let Some(len) = lp.0.to_length() { + let px = len.px(); + if px > 0.0 { + dims.layout_min_height = Some(px); } } - _ => {} } // max-width - match &pos.max_width { - GenericMaxSize::LengthPercentage(lp) => { - if let Some(len) = lp.0.to_length() { - dims.layout_max_width = Some(len.px()); - } + if let GenericMaxSize::LengthPercentage(lp) = &pos.max_width { + if let Some(len) = lp.0.to_length() { + dims.layout_max_width = Some(len.px()); } - _ => {} } // max-height - match &pos.max_height { - GenericMaxSize::LengthPercentage(lp) => { - if let Some(len) = lp.0.to_length() { - dims.layout_max_height = Some(len.px()); - } + if let GenericMaxSize::LengthPercentage(lp) = &pos.max_height { + if let Some(len) = lp.0.to_length() { + dims.layout_max_height = Some(len.px()); } - _ => {} } } diff --git a/crates/grida-canvas/src/htmlcss/collect.rs b/crates/grida-canvas/src/htmlcss/collect.rs index ed3806c027..a9d5750dc4 100644 --- a/crates/grida-canvas/src/htmlcss/collect.rs +++ b/crates/grida-canvas/src/htmlcss/collect.rs @@ -31,7 +31,7 @@ pub fn collect_styled_tree(html: &str) -> Result, String> use csscascade::cascade::CascadeDriver; use style::thread_state::{self, ThreadState}; - let _ = thread_state::initialize(ThreadState::LAYOUT); + thread_state::initialize(ThreadState::LAYOUT); // Enable CSS Grid support in Stylo's servo mode (one-time). // Without this, `display: grid` is not parsed (gated behind a pref). @@ -46,7 +46,7 @@ pub fn collect_styled_tree(html: &str) -> Result, String> driver.flush(document); let _styled_count = driver.style_document(document); - let root = document.root_element().map(|el| collect_element(el)); + let root = document.root_element().map(collect_element); Ok(root) } @@ -96,14 +96,14 @@ fn generate_marker_text(lst: &T, ordinal: i32) -> Option= 1 && ordinal <= 26 { + if (1..=26).contains(&ordinal) { let ch = (b'a' + (ordinal - 1) as u8) as char; return Some(format!("{}. ", ch)); } return Some(format!("{}. ", ordinal)); } if debug.contains("UpperAlpha") { - if ordinal >= 1 && ordinal <= 26 { + if (1..=26).contains(&ordinal) { let ch = (b'A' + (ordinal - 1) as u8) as char; return Some(format!("{}. ", ch)); } @@ -667,7 +667,7 @@ fn extract_border(style: &ComputedValues) -> BorderBox { let extract_color = |color: &style::values::computed::Color| -> CGColor { color .as_absolute() - .map(|abs| abs_color_to_cg(abs)) + .map(abs_color_to_cg) .unwrap_or(CGColor::BLACK) }; @@ -770,8 +770,8 @@ fn extract_background(style: &ComputedValues) -> Vec { // 2. Background image layers (gradients on top) for image in bg.background_image.0.iter() { - match image { - GenericImage::Gradient(gradient) => match gradient.as_ref() { + if let GenericImage::Gradient(gradient) = image { + match gradient.as_ref() { GenericGradient::Linear { direction, items, .. } => { @@ -799,8 +799,7 @@ fn extract_background(style: &ComputedValues) -> Vec { } layers.push(BackgroundLayer::ConicGradient(ConicGradient { stops })); } - }, - _ => {} + } } } @@ -853,7 +852,7 @@ fn gradient_items_to_stops( GenericGradientItem::SimpleColorStop(color) => { let c = color .as_absolute() - .map(|a| abs_color_to_cg(a)) + .map(abs_color_to_cg) .unwrap_or(CGColor::TRANSPARENT); raw.push((None, c)); } @@ -861,7 +860,7 @@ fn gradient_items_to_stops( let offset = position.to_percentage().map(|p| p.0); let c = color .as_absolute() - .map(|a| abs_color_to_cg(a)) + .map(abs_color_to_cg) .unwrap_or(CGColor::TRANSPARENT); raw.push((offset, c)); } @@ -893,7 +892,7 @@ fn conic_gradient_items_to_stops( GenericGradientItem::SimpleColorStop(color) => { let c = color .as_absolute() - .map(|a| abs_color_to_cg(a)) + .map(abs_color_to_cg) .unwrap_or(CGColor::TRANSPARENT); raw.push((None, c)); } @@ -904,7 +903,7 @@ fn conic_gradient_items_to_stops( }; let c = color .as_absolute() - .map(|a| abs_color_to_cg(a)) + .map(abs_color_to_cg) .unwrap_or(CGColor::TRANSPARENT); raw.push((offset, c)); } @@ -947,6 +946,7 @@ fn auto_distribute_stops(raw: &mut [(Option, CGColor)]) { let s = raw[start].0.unwrap(); let e = raw[end].0.unwrap(); let count = (end - start) as f32; + #[allow(clippy::needless_range_loop)] for j in (start + 1)..end { raw[j].0 = Some(s + (j - start) as f32 / count * (e - s)); } @@ -964,7 +964,7 @@ fn extract_box_shadow(style: &ComputedValues) -> Vec { .base .color .as_absolute() - .map(|a| abs_color_to_cg(a)) + .map(abs_color_to_cg) .unwrap_or(CGColor::BLACK); BoxShadow { offset_x: s.base.horizontal.px(), @@ -1008,11 +1008,8 @@ fn extract_grid_template( RepeatCount::AutoFill => types::RepeatCount::AutoFill, RepeatCount::AutoFit => types::RepeatCount::AutoFit, }; - let tracks: Vec = rep - .track_sizes - .iter() - .map(|ts| stylo_track_size(ts)) - .collect(); + let tracks: Vec = + rep.track_sizes.iter().map(stylo_track_size).collect(); entries.push(types::GridTemplateEntry::Repeat(count, tracks)); } } @@ -1028,7 +1025,7 @@ fn extract_implicit_tracks( style::values::generics::grid::GenericTrackSize, >, ) -> Vec { - tracks.0.iter().map(|ts| stylo_track_size(ts)).collect() + tracks.0.iter().map(stylo_track_size).collect() } /// Convert a single Stylo `TrackSize` to our IR. @@ -1108,13 +1105,7 @@ fn extract_font(style: &ComputedValues) -> FontProps { let font = style.get_font(); let inherited_text = style.get_inherited_text(); - let mut props = FontProps::default(); - - props.size = font.font_size.computed_size().px(); - props.weight = FontWeight(font.font_weight.value() as u32); - props.italic = font.font_style == style::values::computed::FontStyle::ITALIC; - - props.families = font + let families: Vec = font .font_family .families .iter() @@ -1127,22 +1118,35 @@ fn extract_font(style: &ComputedValues) -> FontProps { }) .collect(); - props.line_height = match &font.line_height { + let line_height = match &font.line_height { StyloLineHeight::Normal => LineHeight::Normal, StyloLineHeight::Number(n) => LineHeight::Number(n.0), StyloLineHeight::Length(len) => LineHeight::Px(len.0.px()), }; - // Letter/word spacing - if let Some(len) = inherited_text.letter_spacing.0.to_length() { - props.letter_spacing = len.px(); - } - props.word_spacing = inherited_text + let letter_spacing = inherited_text + .letter_spacing + .0 + .to_length() + .map(|l| l.px()) + .unwrap_or(0.0); + let word_spacing = inherited_text .word_spacing .to_length() .map(|l| l.px()) .unwrap_or(0.0); + let mut props = FontProps { + size: font.font_size.computed_size().px(), + weight: FontWeight(font.font_weight.value() as u32), + italic: font.font_style == style::values::computed::FontStyle::ITALIC, + families, + line_height, + letter_spacing, + word_spacing, + ..Default::default() + }; + // Text align use style::values::specified::text::TextAlignKeyword; props.text_align = match inherited_text.text_align { @@ -1246,22 +1250,12 @@ fn extract_transform(style: &ComputedValues) -> Vec { let mut result = Vec::with_capacity(ops.len()); for op in ops.iter() { let ir_op = match op { - TransformOperation::Matrix(mat) => TransformOp::Matrix([ - mat.a as f32, - mat.b as f32, - mat.c as f32, - mat.d as f32, - mat.e as f32, - mat.f as f32, - ]), - TransformOperation::Matrix3D(mat) => TransformOp::Matrix([ - mat.m11 as f32, - mat.m12 as f32, - mat.m21 as f32, - mat.m22 as f32, - mat.m41 as f32, - mat.m42 as f32, - ]), + TransformOperation::Matrix(mat) => { + TransformOp::Matrix([mat.a, mat.b, mat.c, mat.d, mat.e, mat.f]) + } + TransformOperation::Matrix3D(mat) => { + TransformOp::Matrix([mat.m11, mat.m12, mat.m21, mat.m22, mat.m41, mat.m42]) + } TransformOperation::Translate(tx, ty) | TransformOperation::Translate3D(tx, ty, _) => { TransformOp::Translate(resolve_lp(tx), resolve_lp(ty)) } @@ -1272,18 +1266,16 @@ fn extract_transform(style: &ComputedValues) -> Vec { TransformOp::Translate(LP::Px(0.0), resolve_lp(ty)) } TransformOperation::Scale(sx, sy) | TransformOperation::Scale3D(sx, sy, _) => { - TransformOp::Scale(*sx as f32, *sy as f32) + TransformOp::Scale(*sx, *sy) } - TransformOperation::ScaleX(sx) => TransformOp::Scale(*sx as f32, 1.0), - TransformOperation::ScaleY(sy) => TransformOp::Scale(1.0, *sy as f32), + TransformOperation::ScaleX(sx) => TransformOp::Scale(*sx, 1.0), + TransformOperation::ScaleY(sy) => TransformOp::Scale(1.0, *sy), TransformOperation::Rotate(angle) | TransformOperation::RotateZ(angle) => { - TransformOp::Rotate(angle.radians() as f32) - } - TransformOperation::Skew(ax, ay) => { - TransformOp::Skew(ax.radians() as f32, ay.radians() as f32) + TransformOp::Rotate(angle.radians()) } - TransformOperation::SkewX(ax) => TransformOp::Skew(ax.radians() as f32, 0.0), - TransformOperation::SkewY(ay) => TransformOp::Skew(0.0, ay.radians() as f32), + TransformOperation::Skew(ax, ay) => TransformOp::Skew(ax.radians(), ay.radians()), + TransformOperation::SkewX(ax) => TransformOp::Skew(ax.radians(), 0.0), + TransformOperation::SkewY(ay) => TransformOp::Skew(0.0, ay.radians()), // Z-only and 3D-only ops have no 2D effect _ => continue, }; diff --git a/crates/grida-canvas/src/htmlcss/layout.rs b/crates/grida-canvas/src/htmlcss/layout.rs index 3fd6aa4aa7..f2ce21423c 100644 --- a/crates/grida-canvas/src/htmlcss/layout.rs +++ b/crates/grida-canvas/src/htmlcss/layout.rs @@ -108,6 +108,7 @@ pub fn compute_content_height( // ─── Taffy tree construction ───────────────────────────────────────── +#[allow(clippy::only_used_in_recursion)] fn build_taffy_node( taffy: &mut TaffyTree, el: &StyledElement, @@ -474,7 +475,7 @@ fn grid_template_to_taffy( types::RepeatCount::AutoFill => taffy::RepetitionCount::AutoFill, types::RepeatCount::AutoFit => taffy::RepetitionCount::AutoFit, }, - tracks: tracks.iter().map(|ts| track_size_to_taffy(ts)).collect(), + tracks: tracks.iter().map(track_size_to_taffy).collect(), line_names: Vec::new(), }) } @@ -483,7 +484,7 @@ fn grid_template_to_taffy( } fn implicit_tracks_to_taffy(tracks: &[types::TrackSize]) -> Vec { - tracks.iter().map(|ts| track_size_to_taffy(ts)).collect() + tracks.iter().map(track_size_to_taffy).collect() } fn grid_placement_to_taffy(p: types::GridPlacement) -> taffy::GridPlacement { diff --git a/crates/grida-canvas/src/htmlcss/paint.rs b/crates/grida-canvas/src/htmlcss/paint.rs index 1b530d845b..20349ff109 100644 --- a/crates/grida-canvas/src/htmlcss/paint.rs +++ b/crates/grida-canvas/src/htmlcss/paint.rs @@ -524,7 +524,7 @@ fn paint_box_shadow_inset(canvas: &Canvas, style: &StyledElement, w: f32, h: f32 ]; let mut inner_rrect = skia_safe::RRect::new(); inner_rrect.set_rect_radii(inner_rect, &inner_radii); - builder.add_rrect(&inner_rrect, None, None); + builder.add_rrect(inner_rrect, None, None); } let path = builder.detach(); diff --git a/crates/grida-canvas/src/htmlcss/style.rs b/crates/grida-canvas/src/htmlcss/style.rs index 5090475291..6ea7b20eab 100644 --- a/crates/grida-canvas/src/htmlcss/style.rs +++ b/crates/grida-canvas/src/htmlcss/style.rs @@ -113,6 +113,7 @@ pub struct StyledElement { } /// A node in the styled tree. +#[allow(clippy::large_enum_variant)] #[derive(Debug, Clone)] pub enum StyledNode { Element(StyledElement), diff --git a/crates/grida-canvas/src/htmlcss/types.rs b/crates/grida-canvas/src/htmlcss/types.rs index 081d10f097..6ec2f35c87 100644 --- a/crates/grida-canvas/src/htmlcss/types.rs +++ b/crates/grida-canvas/src/htmlcss/types.rs @@ -135,19 +135,14 @@ pub enum CssLength { } /// CSS line-height (inherited text property). -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq, Default)] pub enum LineHeight { + #[default] Normal, Number(f32), Px(f32), } -impl Default for LineHeight { - fn default() -> Self { - Self::Normal - } -} - /// CSS `float` property. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum Float { diff --git a/crates/grida-canvas/src/io/id_converter.rs b/crates/grida-canvas/src/io/id_converter.rs index de469df3d2..ef3508ae36 100644 --- a/crates/grida-canvas/src/io/id_converter.rs +++ b/crates/grida-canvas/src/io/id_converter.rs @@ -74,10 +74,7 @@ impl IdConverter { // Extract scene metadata from SceneNode let (scene_name, bg_color) = if let Some(JSONNode::Scene(scene_node)) = file.document.nodes.get(&scene_id) { - ( - scene_node.name.clone(), - Some(scene_node.background_color.clone()), - ) + (scene_node.name.clone(), Some(scene_node.background_color)) } else { (scene_id.clone(), None) }; diff --git a/crates/grida-canvas/src/io/io_css.rs b/crates/grida-canvas/src/io/io_css.rs index 44ee9c19d0..ff8b855f6a 100644 --- a/crates/grida-canvas/src/io/io_css.rs +++ b/crates/grida-canvas/src/io/io_css.rs @@ -27,20 +27,15 @@ use crate::cg::prelude::*; use serde::Deserialize; use serde_json::Value; -#[derive(Debug, PartialEq, Deserialize)] +#[derive(Debug, PartialEq, Deserialize, Default)] pub enum CSSPosition { #[serde(rename = "relative")] + #[default] Relative, #[serde(rename = "absolute")] Absolute, } -impl Default for CSSPosition { - fn default() -> Self { - CSSPosition::Relative - } -} - impl From for LayoutPositioning { fn from(position: CSSPosition) -> Self { match position { @@ -137,9 +132,10 @@ where } } -#[derive(Debug, PartialEq, Deserialize)] +#[derive(Debug, PartialEq, Deserialize, Default)] pub enum CSSFontKerning { #[serde(rename = "auto")] + #[default] Auto, #[serde(rename = "normal")] Normal, @@ -147,12 +143,6 @@ pub enum CSSFontKerning { None, } -impl Default for CSSFontKerning { - fn default() -> Self { - CSSFontKerning::Auto - } -} - pub trait UserAgentAutoTaste { /// converts a enum with "auto", that requires user agent default behaviour, to a boolean flag. fn to_flag_with_auto(&self) -> bool; @@ -168,11 +158,12 @@ impl UserAgentAutoTaste for CSSFontKerning { } } -#[derive(Debug, PartialEq, Deserialize)] +#[derive(Debug, PartialEq, Deserialize, Default)] pub enum CSSObjectFit { #[serde(rename = "cover")] Cover, #[serde(rename = "contain")] + #[default] Contain, #[serde(rename = "fill")] Fill, @@ -183,12 +174,6 @@ pub enum CSSObjectFit { // ScaleDown, } -impl Default for CSSObjectFit { - fn default() -> Self { - CSSObjectFit::Contain - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/grida-canvas/src/io/io_grida.rs b/crates/grida-canvas/src/io/io_grida.rs index c1e3926c85..01bcb765ea 100644 --- a/crates/grida-canvas/src/io/io_grida.rs +++ b/crates/grida-canvas/src/io/io_grida.rs @@ -41,7 +41,7 @@ impl From for GradientStop { fn from(stop: JSONGradientStop) -> Self { GradientStop { offset: stop.offset, - color: stop.color.into(), + color: stop.color, } } } @@ -288,7 +288,7 @@ impl From for FeShadow { dy: box_shadow.dy, blur: box_shadow.blur, spread: box_shadow.spread, - color: box_shadow.color.into(), + color: box_shadow.color, active: box_shadow.active, } } @@ -359,11 +359,8 @@ fn default_num_octaves() -> i32 { impl From for NoiseEffectColors { fn from(json: JSONFeNoiseColors) -> Self { match json { - JSONFeNoiseColors::Mono { color } => NoiseEffectColors::Mono { color: color }, - JSONFeNoiseColors::Duo { color1, color2 } => NoiseEffectColors::Duo { - color1: color1, - color2: color2, - }, + JSONFeNoiseColors::Mono { color } => NoiseEffectColors::Mono { color }, + JSONFeNoiseColors::Duo { color1, color2 } => NoiseEffectColors::Duo { color1, color2 }, JSONFeNoiseColors::Multi { opacity } => NoiseEffectColors::Multi { opacity }, } } @@ -391,7 +388,7 @@ impl From> for Paint { blend_mode, active, }) => Paint::Solid(SolidPaint { - color: color, + color, blend_mode, active, }), @@ -520,11 +517,7 @@ impl From for VarWidthProfile { fn from(profile: JSONVariableWidthProfile) -> Self { VarWidthProfile { base: 1.0, // TODO: need to use node's stroke width as base - stops: profile - .stops - .into_iter() - .map(|s| WidthStop::from(s)) - .collect(), + stops: profile.stops.into_iter().collect(), } } } @@ -926,7 +919,7 @@ impl JSONNode { } /// JSON representation of LayoutMode for deserialization -#[derive(Debug, Deserialize, Clone, Copy)] +#[derive(Debug, Deserialize, Clone, Copy, Default)] pub enum JSONLayoutMode { /// Legacy - will be removed, replaced with Normal #[serde(rename = "flow")] @@ -934,15 +927,10 @@ pub enum JSONLayoutMode { #[serde(rename = "flex")] Flex, #[serde(rename = "normal")] + #[default] Normal, } -impl Default for JSONLayoutMode { - fn default() -> Self { - JSONLayoutMode::Normal - } -} - impl From for LayoutMode { fn from(mode: JSONLayoutMode) -> Self { match mode { @@ -956,17 +944,13 @@ impl From for LayoutMode { /// JSON representation of Axis for deserialization #[derive(Debug, Deserialize, Clone, Copy)] #[serde(rename_all = "lowercase")] +#[derive(Default)] pub enum JSONAxis { + #[default] Horizontal, Vertical, } -impl Default for JSONAxis { - fn default() -> Self { - JSONAxis::Horizontal - } -} - impl From for Axis { fn from(axis: JSONAxis) -> Self { match axis { @@ -1496,7 +1480,7 @@ impl From for TextSpanNodeRec { text_decoration_skip_ink: node.text_decoration_skip_ink, text_decoration_thickness: node.text_decoration_thickness, }), - font_family: node.font_family.unwrap_or_else(|| "".to_string()), + font_family: node.font_family.unwrap_or_default(), font_size: node.font_size.unwrap_or(14.0), font_weight: node.font_weight, font_width: node.font_width, @@ -1747,7 +1731,8 @@ impl From for Node { active, }) => { let resolved = h.unwrap_or_else(|| url.clone()); - let image_paint = ImagePaint { + + ImagePaint { image: ResourceRef::RID(resolved), quarter_turns: quarter_turns % 4, alignement: Alignment::CENTER, @@ -1756,9 +1741,7 @@ impl From for Node { blend_mode, filters, active, - }; - - image_paint + } } _ => ImagePaint { image: ResourceRef::RID(url.clone()), @@ -2201,7 +2184,7 @@ fn json_paint_to_image_paint_fit( // Handle tile mode using the separate scale and repeat fields ImagePaintFit::Tile(ImageTile { scale: scale.unwrap_or(1.0), - repeat: repeat.map(|r| r.into()).unwrap_or(ImageRepeat::Repeat), + repeat: repeat.unwrap_or(ImageRepeat::Repeat), }) } Some("contain") => ImagePaintFit::Fit(BoxFit::Contain), @@ -2564,9 +2547,10 @@ fn merge_effects( } /// Flattened JSON representation of LayerBlendMode for easier deserialization -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Default)] pub enum JSONLayerBlendMode { #[serde(rename = "pass-through")] + #[default] PassThrough, #[serde(rename = "normal")] Normal, @@ -2602,15 +2586,9 @@ pub enum JSONLayerBlendMode { Luminosity, } -impl Default for JSONLayerBlendMode { - fn default() -> Self { - JSONLayerBlendMode::PassThrough - } -} - -impl Into for JSONLayerBlendMode { - fn into(self) -> LayerBlendMode { - match self { +impl From for LayerBlendMode { + fn from(val: JSONLayerBlendMode) -> Self { + match val { JSONLayerBlendMode::PassThrough => LayerBlendMode::PassThrough, JSONLayerBlendMode::Normal => LayerBlendMode::Blend(BlendMode::Normal), JSONLayerBlendMode::Multiply => LayerBlendMode::Blend(BlendMode::Multiply), @@ -2643,9 +2621,9 @@ pub enum JSONLayerMaskType { Luminance, } -impl Into for JSONLayerMaskType { - fn into(self) -> LayerMaskType { - match self { +impl From for LayerMaskType { + fn from(val: JSONLayerMaskType) -> Self { + match val { JSONLayerMaskType::Geometry => LayerMaskType::Geometry, JSONLayerMaskType::Alpha => LayerMaskType::Image(ImageMaskType::Alpha), JSONLayerMaskType::Luminance => LayerMaskType::Image(ImageMaskType::Luminance), diff --git a/crates/grida-canvas/src/io/io_grida_fbs.rs b/crates/grida-canvas/src/io/io_grida_fbs.rs index baa0c6e386..bb0a4a0dbc 100644 --- a/crates/grida-canvas/src/io/io_grida_fbs.rs +++ b/crates/grida-canvas/src/io/io_grida_fbs.rs @@ -382,7 +382,7 @@ fn decode_all_inner(bytes: &[u8]) -> Result { decode_markdown_embed_node ); } - fbs::Node::UnknownNode | fbs::Node::NONE | _ => {} + _ => {} } } } @@ -405,7 +405,7 @@ fn decode_all_inner(bytes: &[u8]) -> Result { } } for children in children_by_parent.values_mut() { - children.sort_by(|a, b| a.1.cmp(&b.1)); + children.sort_by(|a, b| a.1.cmp(b.1)); } // Build internal_links from children_by_parent BEFORE consuming @@ -646,9 +646,7 @@ fn decode_text_style_rec_fbs(ts: fbs::TextStyleRec<'_>) -> TextStyleRec { .unwrap_or(FontOpticalSizing::Auto); rec.text_decoration = ts.text_decoration().map(|td| TextDecorationRec { text_decoration_line: decode_text_decoration_line(td.text_decoration_line()), - text_decoration_color: td - .text_decoration_color() - .map(|c| decode_rgba32f_to_cg_color(c)), + text_decoration_color: td.text_decoration_color().map(decode_rgba32f_to_cg_color), text_decoration_style: Some(decode_text_decoration_style(td.text_decoration_style())), text_decoration_skip_ink: Some(td.text_decoration_skip_ink()), text_decoration_thickness: { @@ -903,13 +901,10 @@ fn decode_paints_vec( /// Checks the `blur_type` discriminant first; falls back to Gaussian if /// the variant is unrecognised or the payload is missing. fn decode_fe_blur_from_layer(lb: &fbs::FeLayerBlur<'_>) -> FeBlur { - match lb.blur_type() { - fbs::FeBlur::FeProgressiveBlur => { - if let Some(p) = lb.blur_as_fe_progressive_blur() { - return decode_progressive_blur(&p); - } + if lb.blur_type() == fbs::FeBlur::FeProgressiveBlur { + if let Some(p) = lb.blur_as_fe_progressive_blur() { + return decode_progressive_blur(&p); } - _ => {} } // Default / Gaussian path FeBlur::Gaussian(FeGaussianBlur { @@ -921,13 +916,10 @@ fn decode_fe_blur_from_layer(lb: &fbs::FeLayerBlur<'_>) -> FeBlur { } fn decode_fe_blur_from_backdrop(bb: &fbs::FeBackdropBlur<'_>) -> FeBlur { - match bb.blur_type() { - fbs::FeBlur::FeProgressiveBlur => { - if let Some(p) = bb.blur_as_fe_progressive_blur() { - return decode_progressive_blur(&p); - } + if bb.blur_type() == fbs::FeBlur::FeProgressiveBlur { + if let Some(p) = bb.blur_as_fe_progressive_blur() { + return decode_progressive_blur(&p); } - _ => {} } FeBlur::Gaussian(FeGaussianBlur { radius: bb @@ -1105,7 +1097,7 @@ fn decode_stroke_style_from_fbs(ss: Option>) -> StrokeStyle stroke_miter_limit: StrokeMiterLimit(ss.stroke_miter_limit()), stroke_dash_array: ss .stroke_dash_array() - .filter(|v| v.len() > 0) + .filter(|v| !v.is_empty()) .map(|v| StrokeDashArray((0..v.len()).map(|i| v.get(i)).collect())), } } @@ -1468,7 +1460,7 @@ fn decode_vector_network(vnd: Option>) -> VectorNetwo .regions() .map(|rs| { (0..rs.len()) - .filter_map(|i| { + .map(|i| { let region = rs.get(i); let loops: Vec = region .region_loops() @@ -1492,11 +1484,11 @@ fn decode_vector_network(vnd: Option>) -> VectorNetwo let fills = region .region_fill_paints() .map(|p| decode_paints_vec(Some(p))); - Some(VectorNetworkRegion { + VectorNetworkRegion { loops, fill_rule, fills, - }) + } }) .collect() }) @@ -1521,7 +1513,7 @@ fn decode_group_node( let (x, y) = layout.as_ref().map(decode_layout_xy).unwrap_or((0.0, 0.0)); let transform = if let Some(plt) = layer.post_layout_transform() { - let mut t = decode_fbs_transform(&plt); + let mut t = decode_fbs_transform(plt); // The layout position (x, y) is stored separately; fold it into // the transform's translation component. t.matrix[0][2] = x; @@ -1902,7 +1894,7 @@ fn decode_line_node(lc: &LayerCommon, layer: &fbs::LayerTrait<'_>, ln: &fbs::Lin .unwrap_or_default(); let stroke_dash_array = sg.as_ref().and_then(|s| s.stroke_style()).and_then(|ss| { ss.stroke_dash_array() - .filter(|v| v.len() > 0) + .filter(|v| !v.is_empty()) .map(|v| StrokeDashArray((0..v.len()).map(|i| v.get(i)).collect())) }); @@ -2308,6 +2300,7 @@ pub fn encode( /// Each entry is `(scene_id, scene, id_map, position_map)`. /// All scenes share the same flat `nodes` vector; each scene's nodes /// are prefixed with a scene-type NodeSlot that references `scene_id`. +#[allow(clippy::type_complexity)] pub fn encode_multi( entries: &[( &str, @@ -3727,10 +3720,7 @@ fn encode_group_node<'a, A: flatbuffers::Allocator + 'a>( // Groups carry the full affine transform (scale, skew, rotation) — not // just rotation like shape nodes. Encode the full matrix so SVG-imported // transforms (e.g. scale(0.8, 1.2)) survive the roundtrip. - let plt = r - .transform - .as_ref() - .map(|t| encode_affine_to_cg_transform(t)); + let plt = r.transform.as_ref().map(encode_affine_to_cg_transform); let sys = encode_system_node_trait(fbb, node_id, "", r.active, false); let layout = encode_shape_layout(fbb, x, y, None, None, &None); @@ -4210,7 +4200,7 @@ fn encode_text_style_rec<'a, A: flatbuffers::Allocator + 'a>( let color = td .text_decoration_color .as_ref() - .map(|c| encode_color_to_rgba32f(c)); + .map(encode_color_to_rgba32f); fbs::TextDecorationRec::create( fbb, &fbs::TextDecorationRecArgs { diff --git a/crates/grida-canvas/src/layout/engine.rs b/crates/grida-canvas/src/layout/engine.rs index a428253c85..f7d1af11b4 100644 --- a/crates/grida-canvas/src/layout/engine.rs +++ b/crates/grida-canvas/src/layout/engine.rs @@ -79,6 +79,12 @@ pub struct LayoutEngine { result: LayoutResult, } +impl Default for LayoutEngine { + fn default() -> Self { + Self::new() + } +} + impl LayoutEngine { pub fn new() -> Self { Self { @@ -403,8 +409,8 @@ impl LayoutEngine { scene_node_id: *node_id, text: n.text.clone(), text_style: n.text_style.clone(), - text_align: n.text_align.clone(), - max_lines: n.max_lines.clone(), + text_align: n.text_align, + max_lines: n.max_lines, ellipsis: n.ellipsis.clone(), width: n.width, height: n.height, @@ -415,8 +421,8 @@ impl LayoutEngine { let ctx = AttributedTextMeasureContext { scene_node_id: *node_id, attributed_string: n.attributed_string.clone(), - text_align: n.text_align.clone(), - max_lines: n.max_lines.clone(), + text_align: n.text_align, + max_lines: n.max_lines, ellipsis: n.ellipsis.clone(), width: n.width, height: n.height, diff --git a/crates/grida-canvas/src/layout/tree.rs b/crates/grida-canvas/src/layout/tree.rs index 19a95e1b36..2a3d2e4384 100644 --- a/crates/grida-canvas/src/layout/tree.rs +++ b/crates/grida-canvas/src/layout/tree.rs @@ -222,7 +222,6 @@ impl LayoutTree { measure_provider: Option<&mut TextMeasureProvider<'_>>, ) -> Result<(), taffy::TaffyError> { if let Some(provider) = measure_provider { - let provider = provider; self.taffy.compute_layout_with_measure( root, available_space, diff --git a/crates/grida-canvas/src/node/factory.rs b/crates/grida-canvas/src/node/factory.rs index c96de91f41..7fa4afeeff 100644 --- a/crates/grida-canvas/src/node/factory.rs +++ b/crates/grida-canvas/src/node/factory.rs @@ -10,6 +10,12 @@ use math2::{box_fit::BoxFit, transform::AffineTransform}; /// Actual IDs should be assigned by SceneRuntime or another ID management system. pub struct NodeFactory; +impl Default for NodeFactory { + fn default() -> Self { + Self::new() + } +} + impl NodeFactory { pub fn new() -> Self { Self {} diff --git a/crates/grida-canvas/src/node/id.rs b/crates/grida-canvas/src/node/id.rs index 68417ff8a5..4b023a95ee 100644 --- a/crates/grida-canvas/src/node/id.rs +++ b/crates/grida-canvas/src/node/id.rs @@ -26,6 +26,7 @@ impl NodeIdGenerator { } /// Generate the next unique node ID. + #[allow(clippy::should_implement_trait)] pub fn next(&mut self) -> NodeId { let id = self.counter; self.counter += 1; diff --git a/crates/grida-canvas/src/node/scene_graph.rs b/crates/grida-canvas/src/node/scene_graph.rs index 6334193f6a..d703049f63 100644 --- a/crates/grida-canvas/src/node/scene_graph.rs +++ b/crates/grida-canvas/src/node/scene_graph.rs @@ -693,13 +693,13 @@ impl SceneGraph { match parent { Parent::Root => { - self.roots.push(id.clone()); + self.roots.push(id); } Parent::NodeId(parent_id) => { if let Some(children) = self.links.get_mut(&parent_id) { - children.push(id.clone()); + children.push(id); } else { - self.links.insert(parent_id, vec![id.clone()]); + self.links.insert(parent_id, vec![id]); } } } @@ -730,7 +730,7 @@ impl SceneGraph { let children = self .links .get_mut(parent) - .ok_or_else(|| SceneGraphError::ParentNotFound(parent.clone()))?; + .ok_or(SceneGraphError::ParentNotFound(*parent))?; children.push(child); Ok(()) } @@ -745,11 +745,11 @@ impl SceneGraph { let children = self .links .get_mut(parent) - .ok_or_else(|| SceneGraphError::ParentNotFound(parent.clone()))?; + .ok_or(SceneGraphError::ParentNotFound(*parent))?; if index > children.len() { return Err(SceneGraphError::IndexOutOfBounds { - parent: parent.clone(), + parent: *parent, index, len: children.len(), }); @@ -764,12 +764,12 @@ impl SceneGraph { let children = self .links .get_mut(parent) - .ok_or_else(|| SceneGraphError::ParentNotFound(parent.clone()))?; + .ok_or(SceneGraphError::ParentNotFound(*parent))?; let pos = children .iter() .position(|id| id == child) - .ok_or_else(|| SceneGraphError::ChildNotFound(child.clone()))?; + .ok_or(SceneGraphError::ChildNotFound(*child))?; children.remove(pos); Ok(()) @@ -807,16 +807,14 @@ impl SceneGraph { /// Get a reference to a node by ID pub fn get_node(&self, id: &NodeId) -> SceneGraphResult<&Node> { - self.nodes - .get(id) - .ok_or_else(|| SceneGraphError::NodeNotFound(id.clone())) + self.nodes.get(id).ok_or(SceneGraphError::NodeNotFound(*id)) } /// Get a mutable reference to a node by ID pub fn get_node_mut(&mut self, id: &NodeId) -> SceneGraphResult<&mut Node> { self.nodes .get_mut(id) - .ok_or_else(|| SceneGraphError::NodeNotFound(id.clone())) + .ok_or(SceneGraphError::NodeNotFound(*id)) } /// Get the display name for a node, if one was set. @@ -834,7 +832,7 @@ impl SceneGraph { self.geo_data.remove(id); self.nodes .remove(id) - .ok_or_else(|| SceneGraphError::NodeNotFound(id.clone())) + .ok_or(SceneGraphError::NodeNotFound(*id)) } /// Check if a node exists in the repository @@ -912,7 +910,7 @@ impl SceneGraph { visitor: &mut impl FnMut(&NodeId), ) -> SceneGraphResult<()> { if !self.has_node(root) { - return Err(SceneGraphError::NodeNotFound(root.clone())); + return Err(SceneGraphError::NodeNotFound(*root)); } visitor(root); @@ -933,7 +931,7 @@ impl SceneGraph { visitor: &mut impl FnMut(&NodeId), ) -> SceneGraphResult<()> { if !self.has_node(root) { - return Err(SceneGraphError::NodeNotFound(root.clone())); + return Err(SceneGraphError::NodeNotFound(*root)); } if let Some(children) = self.get_children(root) { @@ -950,19 +948,19 @@ impl SceneGraph { /// Get all ancestors of a node (path to root) pub fn ancestors(&self, id: &NodeId) -> SceneGraphResult> { if !self.has_node(id) { - return Err(SceneGraphError::NodeNotFound(id.clone())); + return Err(SceneGraphError::NodeNotFound(*id)); } let mut result = Vec::new(); - let mut current = id.clone(); + let mut current = *id; // Find parent by searching all links loop { let mut found_parent = false; for (parent_id, children) in &self.links { if children.contains(¤t) { - result.push(parent_id.clone()); - current = parent_id.clone(); + result.push(parent_id); + current = parent_id; found_parent = true; break; } @@ -979,14 +977,14 @@ impl SceneGraph { /// Get all descendants of a node (all children recursively) pub fn descendants(&self, id: &NodeId) -> SceneGraphResult> { if !self.has_node(id) { - return Err(SceneGraphError::NodeNotFound(id.clone())); + return Err(SceneGraphError::NodeNotFound(*id)); } let mut result = Vec::new(); self.walk_preorder(id, &mut |node_id| { if node_id != id { - result.push(node_id.clone()); + result.push(*node_id); } })?; diff --git a/crates/grida-canvas/src/node/schema.rs b/crates/grida-canvas/src/node/schema.rs index 99e2b5fbef..f499abc291 100644 --- a/crates/grida-canvas/src/node/schema.rs +++ b/crates/grida-canvas/src/node/schema.rs @@ -9,7 +9,7 @@ use math2::transform::AffineTransform; // Re-export the ID types from the id module pub use crate::node::id::{NodeId, NodeIdGenerator, UserNodeId}; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct LayerEffects { /// single layer blur is supported per layer /// layer blur is applied after all other effects @@ -188,24 +188,12 @@ impl LayerEffects { } } -impl Default for LayerEffects { - fn default() -> Self { - Self { - blur: None, - backdrop_blur: None, - shadows: vec![], - glass: None, - noises: vec![], - } - } -} - /// common stroke style /// not used for special node types, /// - line /// - vector /// - text -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct StrokeStyle { pub stroke_align: StrokeAlign, pub stroke_cap: StrokeCap, @@ -214,18 +202,6 @@ pub struct StrokeStyle { pub stroke_dash_array: Option, } -impl Default for StrokeStyle { - fn default() -> Self { - Self { - stroke_align: StrokeAlign::default(), - stroke_cap: StrokeCap::default(), - stroke_join: StrokeJoin::default(), - stroke_miter_limit: StrokeMiterLimit::default(), - stroke_dash_array: None, - } - } -} - impl StrokeStyle { pub fn new() -> Self { Self::default() @@ -869,7 +845,7 @@ impl Default for LayoutPositioningBasis { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct LayoutDimensionStyle { pub layout_target_width: Option, pub layout_target_height: Option, @@ -880,20 +856,6 @@ pub struct LayoutDimensionStyle { pub layout_target_aspect_ratio: Option<(f32, f32)>, } -impl Default for LayoutDimensionStyle { - fn default() -> Self { - Self { - layout_target_width: None, - layout_target_height: None, - layout_min_width: None, - layout_max_width: None, - layout_min_height: None, - layout_max_height: None, - layout_target_aspect_ratio: None, - } - } -} - /// Discriminant tag for the [`Node`] enum — lets hot loops dispatch on node /// type without touching the full 500+ byte `Node` variant. /// @@ -1779,12 +1741,12 @@ impl NodeShapeMixin for RectangleNodeRec { corner_radius: self.corner_radius, }); } - return Shape::OrthogonalSmoothRRect(OrthogonalSmoothRRectShape { + Shape::OrthogonalSmoothRRect(OrthogonalSmoothRRectShape { width: self.size.width, height: self.size.height, corner_radius: self.corner_radius, corner_smoothing: self.corner_smoothing, - }); + }) } fn to_path(&self) -> skia_safe::Path { @@ -1995,7 +1957,7 @@ impl NodeShapeMixin for EllipseNodeRec { height: h, inner_radius_ratio: inner_ratio, start_angle: self.start_angle, - angle: angle, + angle, corner_radius: self.corner_radius.unwrap_or(0.0), }); } diff --git a/crates/grida-canvas/src/painter/effects.rs b/crates/grida-canvas/src/painter/effects.rs index e9a437348d..8678846139 100644 --- a/crates/grida-canvas/src/painter/effects.rs +++ b/crates/grida-canvas/src/painter/effects.rs @@ -36,13 +36,12 @@ fn build_transform_matrix(width: f32, height: f32, rotation_degrees: f32) -> [[f // = T(cx, cy) * R(-angle) * T(-cx, -cy) // // So we build: T(cx, cy) * R(-angle) * T(-cx, -cy) - let matrix = [ + + [ [cos_a, sin_a, cx - cx * cos_a - cy * sin_a], [-sin_a, cos_a, cy + cx * sin_a - cy * cos_a], [0.0, 0.0, 1.0], - ]; - - matrix + ] } // ============================================================================ diff --git a/crates/grida-canvas/src/painter/effects_noise.rs b/crates/grida-canvas/src/painter/effects_noise.rs index cb8cae8676..a53487aa92 100644 --- a/crates/grida-canvas/src/painter/effects_noise.rs +++ b/crates/grida-canvas/src/painter/effects_noise.rs @@ -201,8 +201,8 @@ fn multi_contrast_cf() -> sk::ColorFilter { /// Generate an identity LUT (pass-through) fn identity_lut() -> [u8; 256] { let mut t = [0u8; 256]; - for i in 0..256 { - t[i] = i as u8; + for (i, item) in t.iter_mut().enumerate() { + *item = i as u8; } t } @@ -214,8 +214,8 @@ fn identity_lut() -> [u8; 256] { /// Higher threshold = less noise visible (lower density) fn lut_threshold(threshold: usize) -> [u8; 256] { let mut t = [0u8; 256]; - for i in 0..256 { - t[i] = if i >= threshold { 255 } else { 0 }; + for (i, item) in t.iter_mut().enumerate() { + *item = if i >= threshold { 255 } else { 0 }; } t } @@ -236,8 +236,8 @@ fn lut_duo_pattern1(density: f32) -> [u8; 256] { let end = 127; // midpoint let mut lut = [0u8; 256]; - for i in start..=end { - lut[i] = 255; + for item in lut.iter_mut().take(end + 1).skip(start) { + *item = 255; } lut } @@ -258,8 +258,8 @@ fn lut_duo_pattern2(density: f32) -> [u8; 256] { let end = (127.5 + d / 2.0 * 255.0).round() as usize; let mut lut = [0u8; 256]; - for i in start..=end.min(255) { - lut[i] = 255; + for item in lut.iter_mut().take(end.min(255) + 1).skip(start) { + *item = 255; } lut } diff --git a/crates/grida-canvas/src/painter/geometry.rs b/crates/grida-canvas/src/painter/geometry.rs index 4291a41e0a..cd09a9502a 100644 --- a/crates/grida-canvas/src/painter/geometry.rs +++ b/crates/grida-canvas/src/painter/geometry.rs @@ -52,7 +52,7 @@ impl PainterShape { /// Construct a rounded rectangle shape pub fn from_rrect(rrect: RRect) -> Self { Self { - rect: rrect.rect().clone(), + rect: *rrect.rect(), rect_shape: None, rrect: Some(rrect), oval: None, @@ -72,7 +72,7 @@ impl PainterShape { /// Construct a path-based shape (bounding rect must be provided) pub fn from_path(path: Path) -> Self { Self { - rect: path.bounds().clone(), + rect: *path.bounds(), rect_shape: None, rrect: None, oval: None, diff --git a/crates/grida-canvas/src/painter/image.rs b/crates/grida-canvas/src/painter/image.rs index 69cdb02478..828a3117e2 100644 --- a/crates/grida-canvas/src/painter/image.rs +++ b/crates/grida-canvas/src/painter/image.rs @@ -108,8 +108,7 @@ pub fn image_paint_matrix( // For custom transforms, we handle the complete image-to-container mapping // directly without composing with BoxFit::Fill, which would create double transformation. ImagePaintFit::Transform(transform) => { - let matrix = calculate_raw_transform(transform, oriented_image_size, container_size); - matrix + calculate_raw_transform(transform, oriented_image_size, container_size) } ImagePaintFit::Tile(tile) => { // For tile mode, the scale controls how many tiles fit in the container diff --git a/crates/grida-canvas/src/painter/image_filters.rs b/crates/grida-canvas/src/painter/image_filters.rs index 057f495607..3822de734f 100644 --- a/crates/grida-canvas/src/painter/image_filters.rs +++ b/crates/grida-canvas/src/painter/image_filters.rs @@ -70,7 +70,7 @@ use crate::cg::types::ImageFilters; use skia_safe::{self as sk, color_filters, runtime_effect::RuntimeEffect, ColorMatrix, Data}; /// Conversion functions between normalized and physical values - +/// /// Convert normalized exposure value (-1.0 to 1.0) to physical value (0.25 to 4.0) fn normalized_to_physical_exposure(normalized: f32) -> f32 { // Map [-1.0, 1.0] to [0.25, 4.0] using exponential scaling diff --git a/crates/grida-canvas/src/painter/layer.rs b/crates/grida-canvas/src/painter/layer.rs index 6ac6a79905..a711b57df2 100644 --- a/crates/grida-canvas/src/painter/layer.rs +++ b/crates/grida-canvas/src/painter/layer.rs @@ -100,6 +100,7 @@ impl PainterPictureLayer { } #[derive(Debug, Clone)] +#[allow(clippy::large_enum_variant)] pub enum PainterRenderCommand { Draw(PainterPictureLayer), MaskGroup(PainterMaskGroup), @@ -424,7 +425,7 @@ impl LayerList { shadows: Vec::new(), blur: None, backdrop_blur: effects.backdrop_blur.clone(), - glass: effects.glass.clone(), + glass: effects.glass, noises: effects.noises.clone(), }; (surface, own) @@ -491,7 +492,7 @@ impl LayerList { pub fn from_scene(scene: &Scene, scene_cache: &SceneCache) -> Self { let mut list = LayerList::default(); for id in scene.graph.roots() { - let result = Self::flatten_node(&id, &scene.graph, scene_cache, 1.0, &mut list.layers); + let result = Self::flatten_node(id, &scene.graph, scene_cache, 1.0, &mut list.layers); list.commands.extend(result.commands); } // Build a LUT (id -> index) for picture caching and quick lookup @@ -523,6 +524,10 @@ impl LayerList { self.layers.len() } + pub fn is_empty(&self) -> bool { + self.layers.is_empty() + } + /// Compute a fill path with the stroke region subtracted (PathOp::Difference). /// /// Returns `Some(path)` when stroke overlaps fill and both are present. @@ -626,7 +631,7 @@ impl LayerList { let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { base: PainterPictureLayerBase { - id: id.clone(), + id: *id, z_index: out.len(), opacity, blend_mode: n.blend_mode, @@ -645,7 +650,7 @@ impl LayerList { non_overlapping_fill_path, }); out.push(LayerEntry { - id: id.clone(), + id: *id, layer: layer.clone(), }); @@ -728,7 +733,7 @@ impl LayerList { let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { base: PainterPictureLayerBase { - id: id.clone(), + id: *id, z_index: out.len(), opacity: inner_opacity, blend_mode: inner_blend_mode, @@ -747,7 +752,7 @@ impl LayerList { non_overlapping_fill_path, }); out.push(LayerEntry { - id: id.clone(), + id: *id, layer: layer.clone(), }); @@ -778,7 +783,7 @@ impl LayerList { clip_path.map(|path| path.make_transform(&sk::sk_matrix(transform.matrix))); let surface = PainterRenderSurface { - id: id.clone(), + id: *id, bounds: render_bounds, transform, opacity, @@ -847,7 +852,7 @@ impl LayerList { let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { base: PainterPictureLayerBase { - id: id.clone(), + id: *id, z_index: out.len(), opacity, blend_mode: n.blend_mode, @@ -866,7 +871,7 @@ impl LayerList { non_overlapping_fill_path, }); out.push(LayerEntry { - id: id.clone(), + id: *id, layer: layer.clone(), }); FlattenResult { @@ -915,7 +920,7 @@ impl LayerList { let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { base: PainterPictureLayerBase { - id: id.clone(), + id: *id, z_index: out.len(), opacity: parent_opacity * n.opacity, blend_mode: n.blend_mode, @@ -934,7 +939,7 @@ impl LayerList { non_overlapping_fill_path, }); out.push(LayerEntry { - id: id.clone(), + id: *id, layer: layer.clone(), }); FlattenResult { @@ -977,7 +982,7 @@ impl LayerList { let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { base: PainterPictureLayerBase { - id: id.clone(), + id: *id, z_index: out.len(), opacity: parent_opacity * n.opacity, blend_mode: n.blend_mode, @@ -996,7 +1001,7 @@ impl LayerList { non_overlapping_fill_path, }); out.push(LayerEntry { - id: id.clone(), + id: *id, layer: layer.clone(), }); FlattenResult { @@ -1039,7 +1044,7 @@ impl LayerList { let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { base: PainterPictureLayerBase { - id: id.clone(), + id: *id, z_index: out.len(), opacity: parent_opacity * n.opacity, blend_mode: n.blend_mode, @@ -1058,7 +1063,7 @@ impl LayerList { non_overlapping_fill_path, }); out.push(LayerEntry { - id: id.clone(), + id: *id, layer: layer.clone(), }); FlattenResult { @@ -1101,7 +1106,7 @@ impl LayerList { let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { base: PainterPictureLayerBase { - id: id.clone(), + id: *id, z_index: out.len(), opacity: parent_opacity * n.opacity, blend_mode: n.blend_mode, @@ -1120,7 +1125,7 @@ impl LayerList { non_overlapping_fill_path, }); out.push(LayerEntry { - id: id.clone(), + id: *id, layer: layer.clone(), }); FlattenResult { @@ -1163,7 +1168,7 @@ impl LayerList { let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { base: PainterPictureLayerBase { - id: id.clone(), + id: *id, z_index: out.len(), opacity: parent_opacity * n.opacity, blend_mode: n.blend_mode, @@ -1182,7 +1187,7 @@ impl LayerList { non_overlapping_fill_path, }); out.push(LayerEntry { - id: id.clone(), + id: *id, layer: layer.clone(), }); FlattenResult { @@ -1235,7 +1240,7 @@ impl LayerList { }; let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { base: PainterPictureLayerBase { - id: id.clone(), + id: *id, z_index: out.len(), opacity: parent_opacity * n.opacity, blend_mode: n.blend_mode, @@ -1254,7 +1259,7 @@ impl LayerList { non_overlapping_fill_path: None, }); out.push(LayerEntry { - id: id.clone(), + id: *id, layer: layer.clone(), }); FlattenResult { @@ -1293,7 +1298,7 @@ impl LayerList { let layer = PainterPictureLayer::Text(PainterPictureTextLayer { base: PainterPictureLayerBase { - id: id.clone(), + id: *id, z_index: out.len(), opacity: parent_opacity * n.opacity, blend_mode: n.blend_mode, @@ -1315,11 +1320,11 @@ impl LayerList { text_style: n.text_style.clone(), text_align: n.text_align, text_align_vertical: n.text_align_vertical, - id: id.clone(), + id: *id, attributed_string: None, }); out.push(LayerEntry { - id: id.clone(), + id: *id, layer: layer.clone(), }); FlattenResult { @@ -1362,7 +1367,7 @@ impl LayerList { let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { base: PainterPictureLayerBase { - id: id.clone(), + id: *id, z_index: out.len(), opacity: parent_opacity * n.opacity, blend_mode: n.blend_mode, @@ -1381,7 +1386,7 @@ impl LayerList { non_overlapping_fill_path, }); out.push(LayerEntry { - id: id.clone(), + id: *id, layer: layer.clone(), }); FlattenResult { @@ -1397,7 +1402,7 @@ impl LayerList { let shape = build_shape(node, &bounds); let layer = PainterPictureLayer::Vector(PainterPictureVectorLayer { base: PainterPictureLayerBase { - id: id.clone(), + id: *id, z_index: out.len(), opacity: parent_opacity * n.opacity, blend_mode: n.blend_mode, @@ -1421,7 +1426,7 @@ impl LayerList { marker_end_shape: n.marker_end_shape, }); out.push(LayerEntry { - id: id.clone(), + id: *id, layer: layer.clone(), }); FlattenResult { @@ -1465,7 +1470,7 @@ impl LayerList { let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { base: PainterPictureLayerBase { - id: id.clone(), + id: *id, z_index: out.len(), opacity: parent_opacity * n.opacity, blend_mode: n.blend_mode, @@ -1484,7 +1489,7 @@ impl LayerList { non_overlapping_fill_path, }); out.push(LayerEntry { - id: id.clone(), + id: *id, layer: layer.clone(), }); FlattenResult { @@ -1523,7 +1528,7 @@ impl LayerList { let layer = PainterPictureLayer::Text(PainterPictureTextLayer { base: PainterPictureLayerBase { - id: id.clone(), + id: *id, z_index: out.len(), opacity: parent_opacity * n.opacity, blend_mode: n.blend_mode, @@ -1545,11 +1550,11 @@ impl LayerList { text_style: n.default_style.clone(), text_align: n.text_align, text_align_vertical: n.text_align_vertical, - id: id.clone(), + id: *id, attributed_string: Some(n.attributed_string.clone()), }); out.push(LayerEntry { - id: id.clone(), + id: *id, layer: layer.clone(), }); FlattenResult { @@ -1565,7 +1570,7 @@ impl LayerList { let shape = build_shape(node, &bounds); let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { base: PainterPictureLayerBase { - id: id.clone(), + id: *id, z_index: out.len(), opacity: parent_opacity * n.opacity, blend_mode: LayerBlendMode::PassThrough, @@ -1584,7 +1589,7 @@ impl LayerList { non_overlapping_fill_path: None, }); out.push(LayerEntry { - id: id.clone(), + id: *id, layer: layer.clone(), }); FlattenResult { @@ -1605,7 +1610,7 @@ impl LayerList { // markdown measurement when schema height is None). let layer = PainterPictureLayer::MarkdownEmbed(PainterPictureMarkdownEmbedLayer { base: PainterPictureLayerBase { - id: id.clone(), + id: *id, z_index: out.len(), opacity: parent_opacity * n.opacity, blend_mode: n.blend_mode, @@ -1620,7 +1625,7 @@ impl LayerList { height: bounds.height, }); out.push(LayerEntry { - id: id.clone(), + id: *id, layer: layer.clone(), }); FlattenResult { @@ -1638,7 +1643,7 @@ impl LayerList { let layer = PainterPictureLayer::HtmlEmbed(PainterPictureHtmlEmbedLayer { base: PainterPictureLayerBase { - id: id.clone(), + id: *id, z_index: out.len(), opacity: parent_opacity * n.opacity, blend_mode: n.blend_mode, @@ -1653,7 +1658,7 @@ impl LayerList { height: n.size.height, }); out.push(LayerEntry { - id: id.clone(), + id: *id, layer: layer.clone(), }); FlattenResult { @@ -1692,7 +1697,7 @@ impl LayerList { } } // Flush remaining run (no mask above it) - out_commands.extend(run.into_iter()); + out_commands.extend(run); out_commands } diff --git a/crates/grida-canvas/src/painter/mod.rs b/crates/grida-canvas/src/painter/mod.rs index e301acb13e..076a015cfe 100644 --- a/crates/grida-canvas/src/painter/mod.rs +++ b/crates/grida-canvas/src/painter/mod.rs @@ -1,3 +1,4 @@ +#[allow(clippy::module_inception)] mod painter; pub use painter::*; pub mod effects; diff --git a/crates/grida-canvas/src/painter/painter.rs b/crates/grida-canvas/src/painter/painter.rs index 392263cdfc..dfc5b30583 100644 --- a/crates/grida-canvas/src/painter/painter.rs +++ b/crates/grida-canvas/src/painter/painter.rs @@ -304,7 +304,7 @@ impl<'a> Painter<'a> { // Skip noise entirely — it's expensive and purely decorative. noises: Vec::new(), // Keep glass — it's context-dependent and visually essential. - glass: effects.glass.clone(), + glass: effects.glass, } } @@ -1288,6 +1288,7 @@ impl<'a> Painter<'a> { } } + #[allow(clippy::too_many_arguments)] pub fn draw_text_span( &self, id: &NodeId, @@ -2704,7 +2705,7 @@ impl<'a> Painter<'a> { // Merge shadow + source (None = passthrough source) colorized.and_then(|shadow_layer| { - skia_safe::image_filters::merge([Some(shadow_layer), None].into_iter(), None) + skia_safe::image_filters::merge([Some(shadow_layer), None], None) }) } else { // Fast path: use Skia's built-in drop_shadow (draws shadow + source) diff --git a/crates/grida-canvas/src/painter/painter_debug_node.rs b/crates/grida-canvas/src/painter/painter_debug_node.rs index ce32d93f70..f6a2c36b92 100644 --- a/crates/grida-canvas/src/painter/painter_debug_node.rs +++ b/crates/grida-canvas/src/painter/painter_debug_node.rs @@ -488,12 +488,10 @@ impl<'a> NodePainter<'a> { ); }); }); - } else { - if let Some(children) = graph.get_children(id) { - for child_id in children { - if let Ok(child) = graph.get_node(child_id) { - self.draw_node_recursively(child_id, child, graph, cache); - } + } else if let Some(children) = graph.get_children(id) { + for child_id in children { + if let Ok(child) = graph.get_node(child_id) { + self.draw_node_recursively(child_id, child, graph, cache); } } } diff --git a/crates/grida-canvas/src/painter/shadow.rs b/crates/grida-canvas/src/painter/shadow.rs index cd3e860e27..151eabfefb 100644 --- a/crates/grida-canvas/src/painter/shadow.rs +++ b/crates/grida-canvas/src/painter/shadow.rs @@ -19,8 +19,7 @@ pub fn drop_shadow_image_filter(shadow: &FeShadow) -> sk::ImageFilter { filter = image_filters::blur((shadow.blur, shadow.blur), None, filter, None).unwrap(); } - let filter = image_filters::offset((shadow.dx, shadow.dy), filter, None).unwrap(); - filter + image_filters::offset((shadow.dx, shadow.dy), filter, None).unwrap() } else { // fast path using Skia's drop_shadow filter when no spread is applied let image_filter = image_filters::drop_shadow_only( @@ -81,9 +80,8 @@ pub fn inner_shadow_image_filter(shadow: &FeShadow) -> sk::ImageFilter { let blurred = image_filters::blur((shadow.blur, shadow.blur), None, filter, None).unwrap(); let offset = image_filters::offset((shadow.dx, shadow.dy), blurred, None).unwrap(); let masked = image_filters::blend(BlendMode::DstIn, offset, None, None).unwrap(); - let inner_shadow = image_filters::merge([Some(masked)].into_iter(), None).unwrap(); - inner_shadow + image_filters::merge([Some(masked)], None).unwrap() } /// Draw an inner shadow clipped to the given shape. diff --git a/crates/grida-canvas/src/query/hierarchy.rs b/crates/grida-canvas/src/query/hierarchy.rs index 4764d96f60..e3a1c02bd3 100644 --- a/crates/grida-canvas/src/query/hierarchy.rs +++ b/crates/grida-canvas/src/query/hierarchy.rs @@ -348,10 +348,7 @@ fn select_adjacent_sibling( /// For root nodes, returns all roots. fn siblings_of(graph: &SceneGraph, id: &NodeId) -> Vec { match graph.get_parent(id) { - Some(parent) => graph - .get_children(&parent) - .map(|c| c.clone()) - .unwrap_or_default(), + Some(parent) => graph.get_children(&parent).cloned().unwrap_or_default(), None => graph.roots().to_vec(), } } diff --git a/crates/grida-canvas/src/query/paint.rs b/crates/grida-canvas/src/query/paint.rs index d5398077c8..8c1de743f6 100644 --- a/crates/grida-canvas/src/query/paint.rs +++ b/crates/grida-canvas/src/query/paint.rs @@ -79,7 +79,7 @@ pub fn query_paint_groups( let mut groups: Vec = Vec::new(); let limit_reached = |groups: &Vec, limit: Option| -> bool { - limit.map_or(false, |l| groups.len() >= l) + limit.is_some_and(|l| groups.len() >= l) }; if recursive { diff --git a/crates/grida-canvas/src/runtime/camera.rs b/crates/grida-canvas/src/runtime/camera.rs index afaa753a1a..234bc9904b 100644 --- a/crates/grida-canvas/src/runtime/camera.rs +++ b/crates/grida-canvas/src/runtime/camera.rs @@ -75,7 +75,7 @@ impl CameraChangeKind { /// /// # Fields /// - `transform`: The camera's transform in world space. Its translation corresponds to the -/// world coordinate that appears at the center of the screen. +/// world coordinate that appears at the center of the screen. /// - `size`: The logical size of the viewport in pixels (not affected by zoom). /// /// This shifts the camera's center to the screen center and transforms the scene accordingly. @@ -175,7 +175,7 @@ impl Camera2D { } fn before_change(&mut self) { - self.prev_transform = self.transform.clone(); + self.prev_transform = self.transform; } /// Sync the camera cache whenever the camera is changed. @@ -493,7 +493,7 @@ impl Camera2D { /// processed (plan built, compositor invalidated, etc.) so that subsequent /// frames don't see stale deltas. pub fn consume_change(&mut self) { - self.prev_transform = self.transform.clone(); + self.prev_transform = self.transform; } /// Classify the camera change that occurred between `prev_transform` and diff --git a/crates/grida-canvas/src/runtime/counter.rs b/crates/grida-canvas/src/runtime/counter.rs index e92d9a2800..1fdfc58a8a 100644 --- a/crates/grida-canvas/src/runtime/counter.rs +++ b/crates/grida-canvas/src/runtime/counter.rs @@ -5,6 +5,12 @@ pub struct FrameCounter { next: u64, } +impl Default for FrameCounter { + fn default() -> Self { + Self::new() + } +} + impl FrameCounter { /// Create a new frame counter starting at 0 pub fn new() -> Self { diff --git a/crates/grida-canvas/src/runtime/font_repository.rs b/crates/grida-canvas/src/runtime/font_repository.rs index 98a9a9be3e..ed7ae17299 100644 --- a/crates/grida-canvas/src/runtime/font_repository.rs +++ b/crates/grida-canvas/src/runtime/font_repository.rs @@ -86,7 +86,7 @@ impl FontRepository { } } self.missing.remove(&family); - self.fonts.entry(family).or_insert_with(Vec::new).push(hash); + self.fonts.entry(family).or_default().push(hash); self.bump_generation(); } @@ -103,10 +103,7 @@ impl FontRepository { self.user_provider.register_typeface(tf, Some(family)); } } - self.fonts - .entry(family.to_string()) - .or_insert_with(Vec::new) - .push(hash); + self.fonts.entry(family.to_string()).or_default().push(hash); self.missing.remove(family); self.bump_generation(); } @@ -169,6 +166,11 @@ impl FontRepository { self.fonts.len() } + /// Returns true if no font families are loaded. + pub fn is_empty(&self) -> bool { + self.fonts.is_empty() + } + /// Total number of fonts loaded across all families. pub fn len_total(&self) -> usize { self.fonts.values().map(|fonts| fonts.len()).sum() diff --git a/crates/grida-canvas/src/runtime/scene.rs b/crates/grida-canvas/src/runtime/scene.rs index a14255d8d4..da90c28185 100644 --- a/crates/grida-canvas/src/runtime/scene.rs +++ b/crates/grida-canvas/src/runtime/scene.rs @@ -82,7 +82,7 @@ pub type RequestRedrawCallback = Arc; /// Passed through the entire init chain: TS → C ABI → Application → Renderer. /// Fields here are applied once during construction. Most can also be changed /// at runtime via individual setters on `Renderer` / `ApplicationApi`. -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Default)] pub struct RendererOptions { /// When true, embedded fonts (Geist, GeistMono) will be registered. pub use_embedded_fonts: bool, @@ -100,25 +100,10 @@ pub struct RendererOptions { pub config: super::config::RuntimeRendererConfig, } -impl Default for RendererOptions { - fn default() -> Self { - Self { - use_embedded_fonts: false, - use_system_fonts: false, - config: Default::default(), - } - } -} - pub fn collect_scene_font_families(scene: &Scene) -> HashSet { fn walk(id: &NodeId, graph: &SceneGraph, set: &mut HashSet) { - if let Ok(node) = graph.get_node(id) { - match node { - Node::TextSpan(n) => { - set.insert(n.text_style.font_family.clone()); - } - _ => {} - } + if let Ok(Node::TextSpan(n)) = graph.get_node(id) { + set.insert(n.text_style.font_family.clone()); } if let Some(children) = graph.get_children(id) { for child in children { @@ -129,7 +114,7 @@ pub fn collect_scene_font_families(scene: &Scene) -> HashSet { let mut set = HashSet::new(); for id in scene.graph.roots() { - walk(&id, &scene.graph, &mut set); + walk(id, &scene.graph, &mut set); } set } @@ -200,6 +185,7 @@ pub struct DrawResult { pub live_draw_count: usize, } +#[allow(clippy::large_enum_variant)] pub enum FrameFlushResult { OK(FrameFlushStats), NoPending, @@ -779,11 +765,9 @@ impl Renderer { update_commands(&mut group.content_commands, node_id, text, attributed); } PainterRenderCommand::RenderSurface(ref mut surface) => { - if let Some(ref mut own_layer) = surface.own_layer { - if let PainterPictureLayer::Text(ref mut tl) = own_layer { - if tl.base.id == node_id { - update_text_layer(tl, text, attributed); - } + if let Some(PainterPictureLayer::Text(ref mut tl)) = surface.own_layer { + if tl.base.id == node_id { + update_text_layer(tl, text, attributed); } } update_commands(&mut surface.children, node_id, text, attributed); @@ -1383,9 +1367,9 @@ impl Renderer { None }; - let mut canvas = surface.canvas(); + let canvas = surface.canvas(); let draw = self.draw( - &mut canvas, + canvas, &plan, scene.background_color, width, @@ -1588,7 +1572,7 @@ impl Renderer { #[cfg(feature = "perf")] let _t_fonts_start = crate::sys::perf_now(); let requested = collect_scene_font_families(scene); - self.fonts.set_requested_families(requested.into_iter()); + self.fonts.set_requested_families(requested); #[cfg(feature = "perf")] let _t_fonts = crate::sys::perf_now(); @@ -1770,9 +1754,7 @@ impl Renderer { /// Unlike the legacy `flush()`, this does NOT consult `FrameCounter` /// — the caller (via `FrameLoop`) already decided to render. pub fn flush_with_plan(&mut self, plan: FramePlan) -> Option { - if self.scene.is_none() { - return None; - } + self.scene.as_ref()?; let strategy = self.frame_strategy(plan.stable, plan.camera_change); Some(self.render_frame(plan, strategy)) } @@ -2104,15 +2086,13 @@ impl Renderer { return Some(pic.clone()); } - let Some(bounds) = self.scene_cache.geometry.get_render_bounds(&id) else { - return None; - }; + let bounds = self.scene_cache.geometry.get_render_bounds(id)?; let pic = self.with_recording_with_policy(&bounds, policy, draw); if let Some(pic) = &pic { self.scene_cache .picture - .set_node_picture_variant(id.clone(), variant_key, pic.clone()); + .set_node_picture_variant(*id, variant_key, pic.clone()); } pic } @@ -2199,9 +2179,7 @@ impl Renderer { .layers .get(idx) .and_then(|entry| self.scene_cache.geometry.get_render_bounds(&entry.id)) - .map_or(true, |rb| { - rb.width >= min_world_size || rb.height >= min_world_size - }) + .is_none_or(|rb| rb.width >= min_world_size || rb.height >= min_world_size) }; let indices = if all_visible { @@ -2609,10 +2587,10 @@ impl Renderer { let width = surface.width() as f32; let height = surface.height() as f32; - let mut canvas = surface.canvas(); + let canvas = surface.canvas(); // Export/snapshot: not an interactive frame, so no camera change. let frame = self.frame(self.camera.rect(), 1.0, true, CameraChangeKind::None); - let _ = self.draw_nocache(&mut canvas, &frame, None, width, height); + let _ = self.draw_nocache(canvas, &frame, None, width, height); surface.image_snapshot() } diff --git a/crates/grida-canvas/src/shape/ellipse.rs b/crates/grida-canvas/src/shape/ellipse.rs index e443b2ccdd..0b2d70e66c 100644 --- a/crates/grida-canvas/src/shape/ellipse.rs +++ b/crates/grida-canvas/src/shape/ellipse.rs @@ -27,7 +27,7 @@ pub fn build_ellipse_vector_network_ccw(shape: &EllipseShape) -> VectorNetwork { } fn ellipse_to_vector_network(shape: &EllipseShape, clockwise: bool) -> VectorNetwork { - const KAPPA: f32 = 0.5522847498307936; + const KAPPA: f32 = 0.552_284_8; let rx = shape.width / 2.0; let ry = shape.height / 2.0; let cx = rx; diff --git a/crates/grida-canvas/src/shape/ellipse_ring_sector.rs b/crates/grida-canvas/src/shape/ellipse_ring_sector.rs index 71c55bbeaf..aeb6e5df12 100644 --- a/crates/grida-canvas/src/shape/ellipse_ring_sector.rs +++ b/crates/grida-canvas/src/shape/ellipse_ring_sector.rs @@ -47,12 +47,12 @@ impl EllipticalRingSectorShape { /// Build a closed arc path for [`EllipticalArcShape`]. pub fn build_ring_sector_path(shape: &EllipticalRingSectorShape) -> Path { if shape.corner_radius <= 0.0 { - __build_ring_sector_path_no_corner_with_arc_to(&shape) + __build_ring_sector_path_no_corner_with_arc_to(shape) } else { // TODO: this is a trick for implementing the corner to a ring sector. // do it the right way later. #[allow(deprecated)] - __build_ring_sector_path_with_corner_6subpath(&shape) + __build_ring_sector_path_with_corner_6subpath(shape) } } diff --git a/crates/grida-canvas/src/shape/mod.rs b/crates/grida-canvas/src/shape/mod.rs index 0b5810ffe4..14ad263edd 100644 --- a/crates/grida-canvas/src/shape/mod.rs +++ b/crates/grida-canvas/src/shape/mod.rs @@ -46,26 +46,26 @@ pub enum Shape { RegularPolygon(RegularPolygonShape), } -impl Into for &Shape { - fn into(self) -> skia_safe::Path { - match self { +impl From<&Shape> for skia_safe::Path { + fn from(val: &Shape) -> Self { + match val { Shape::Rect(shape) => shape.into(), - Shape::RRect(shape) => build_rrect_path(&shape), - Shape::OrthogonalSmoothRRect(shape) => build_orthogonal_smooth_rrect_path(&shape), - Shape::SimplePolygon(shape) => build_simple_polygon_path(&shape), - Shape::Ellipse(shape) => build_ellipse_path(&shape), - Shape::EllipticalRingSector(shape) => build_ring_sector_path(&shape), - Shape::EllipticalSector(shape) => build_sector_path(&shape), - Shape::EllipticalRing(shape) => build_ring_path(&shape), - Shape::RegularStarPolygon(shape) => build_star_path(&shape), - Shape::RegularPolygon(shape) => build_regular_polygon_path(&shape), + Shape::RRect(shape) => build_rrect_path(shape), + Shape::OrthogonalSmoothRRect(shape) => build_orthogonal_smooth_rrect_path(shape), + Shape::SimplePolygon(shape) => build_simple_polygon_path(shape), + Shape::Ellipse(shape) => build_ellipse_path(shape), + Shape::EllipticalRingSector(shape) => build_ring_sector_path(shape), + Shape::EllipticalSector(shape) => build_sector_path(shape), + Shape::EllipticalRing(shape) => build_ring_path(shape), + Shape::RegularStarPolygon(shape) => build_star_path(shape), + Shape::RegularPolygon(shape) => build_regular_polygon_path(shape), } } } -impl Into for &Shape { - fn into(self) -> VectorNetwork { - match self { +impl From<&Shape> for VectorNetwork { + fn from(val: &Shape) -> Self { + match val { Shape::Rect(shape) => build_rect_vector_network(shape), Shape::RRect(shape) => build_rrect_vector_network(shape), Shape::OrthogonalSmoothRRect(shape) => { diff --git a/crates/grida-canvas/src/shape/rect.rs b/crates/grida-canvas/src/shape/rect.rs index b70db6d799..b5cc48d2f8 100644 --- a/crates/grida-canvas/src/shape/rect.rs +++ b/crates/grida-canvas/src/shape/rect.rs @@ -7,16 +7,16 @@ pub struct RectShape { pub height: f32, } -impl Into for &RectShape { - fn into(self) -> skia_safe::Rect { - skia_safe::Rect::from_wh(self.width, self.height) +impl From<&RectShape> for skia_safe::Rect { + fn from(val: &RectShape) -> Self { + skia_safe::Rect::from_wh(val.width, val.height) } } -impl Into for &RectShape { - fn into(self) -> skia_safe::Path { - let rect: skia_safe::Rect = self.into(); - skia_safe::Path::rect(&rect, None) +impl From<&RectShape> for skia_safe::Path { + fn from(val: &RectShape) -> Self { + let rect: skia_safe::Rect = val.into(); + skia_safe::Path::rect(rect, None) } } diff --git a/crates/grida-canvas/src/shape/regular_star.rs b/crates/grida-canvas/src/shape/regular_star.rs index 84395684ea..458ed0b788 100644 --- a/crates/grida-canvas/src/shape/regular_star.rs +++ b/crates/grida-canvas/src/shape/regular_star.rs @@ -45,11 +45,11 @@ pub fn build_star_points(shape: &RegularStarShape) -> Vec { points } -impl Into for RegularStarShape { - fn into(self) -> SimplePolygonShape { +impl From for SimplePolygonShape { + fn from(val: RegularStarShape) -> Self { SimplePolygonShape { - points: build_star_points(&self), - corner_radius: self.corner_radius, + points: build_star_points(&val), + corner_radius: val.corner_radius, } } } diff --git a/crates/grida-canvas/src/shape/rrect.rs b/crates/grida-canvas/src/shape/rrect.rs index b73529944f..888445c514 100644 --- a/crates/grida-canvas/src/shape/rrect.rs +++ b/crates/grida-canvas/src/shape/rrect.rs @@ -10,9 +10,9 @@ pub struct RRectShape { pub corner_radius: RectangularCornerRadius, } -impl Into for &RRectShape { - fn into(self) -> skia_safe::RRect { - build_rrect(self) +impl From<&RRectShape> for skia_safe::RRect { + fn from(val: &RRectShape) -> Self { + build_rrect(val) } } diff --git a/crates/grida-canvas/src/shape/vector.rs b/crates/grida-canvas/src/shape/vector.rs index 3588110404..60f92a20fb 100644 --- a/crates/grida-canvas/src/shape/vector.rs +++ b/crates/grida-canvas/src/shape/vector.rs @@ -15,6 +15,12 @@ pub struct VectorGeometryShape { pub corner_radius: f32, } +impl Default for VectorGeometryShape { + fn default() -> Self { + Self::new() + } +} + impl VectorGeometryShape { pub fn new() -> Self { Self { diff --git a/crates/grida-canvas/src/sk/mappings.rs b/crates/grida-canvas/src/sk/mappings.rs index f295aeac86..95ce509961 100644 --- a/crates/grida-canvas/src/sk/mappings.rs +++ b/crates/grida-canvas/src/sk/mappings.rs @@ -35,10 +35,10 @@ impl From for skia_safe::TileMode { } } -impl Into for BlendMode { - fn into(self) -> skia_safe::Blender { +impl From for skia_safe::Blender { + fn from(val: BlendMode) -> Self { use skia_safe::BlendMode::*; - let sk_blend_mode = match self { + let sk_blend_mode = match val { BlendMode::Normal => SrcOver, BlendMode::Multiply => Multiply, BlendMode::Screen => Screen, @@ -84,9 +84,9 @@ impl From for skia_safe::BlendMode { } } -impl Into for StrokeCap { - fn into(self) -> skia_safe::PaintCap { - match self { +impl From for skia_safe::PaintCap { + fn from(val: StrokeCap) -> Self { + match val { StrokeCap::Butt => skia_safe::PaintCap::Butt, StrokeCap::Round => skia_safe::PaintCap::Round, StrokeCap::Square => skia_safe::PaintCap::Square, @@ -94,9 +94,9 @@ impl Into for StrokeCap { } } -impl Into for StrokeJoin { - fn into(self) -> skia_safe::PaintJoin { - match self { +impl From for skia_safe::PaintJoin { + fn from(val: StrokeJoin) -> Self { + match val { StrokeJoin::Miter => skia_safe::PaintJoin::Miter, StrokeJoin::Round => skia_safe::PaintJoin::Round, StrokeJoin::Bevel => skia_safe::PaintJoin::Bevel, diff --git a/crates/grida-canvas/src/surface/selection.rs b/crates/grida-canvas/src/surface/selection.rs index 90c54cca91..3095982ffd 100644 --- a/crates/grida-canvas/src/surface/selection.rs +++ b/crates/grida-canvas/src/surface/selection.rs @@ -74,7 +74,7 @@ impl SelectionState { if seen.contains(id) { false } else { - seen.push(id.clone()); + seen.push(*id); true } }); diff --git a/crates/grida-canvas/src/surface/ui/render.rs b/crates/grida-canvas/src/surface/ui/render.rs index 04f883f545..e2ab82a0e3 100644 --- a/crates/grida-canvas/src/surface/ui/render.rs +++ b/crates/grida-canvas/src/surface/ui/render.rs @@ -514,7 +514,7 @@ impl SurfaceUI { continue; } - let label: &str = name.as_deref().unwrap_or_else(|| match variant { + let label: &str = name.as_deref().unwrap_or(match variant { LabelVariant::Badge => "Tray", LabelVariant::Plain => "Container", }); diff --git a/crates/grida-canvas/src/svg/from_usvg_tree.rs b/crates/grida-canvas/src/svg/from_usvg_tree.rs index f132e7e758..771d02b5bd 100644 --- a/crates/grida-canvas/src/svg/from_usvg_tree.rs +++ b/crates/grida-canvas/src/svg/from_usvg_tree.rs @@ -7,9 +7,11 @@ use skia_safe::Path as SkPath; use usvg; pub fn into_tree(svg_source: &str) -> Result { - let mut options = usvg::Options::default(); - options.font_family = geist::FAMILY.to_string(); // our builtin font - options.font_size = 16.0; // font-size default is 'medium' (16px) - based on browser spec + let mut options = usvg::Options { + font_family: geist::FAMILY.to_string(), // our builtin font + font_size: 16.0, // font-size default is 'medium' (16px) - based on browser spec + ..Default::default() + }; // Register embedded font so usvg can layout (it silently drops // text nodes when no font is available). diff --git a/crates/grida-canvas/src/svg/pack.rs b/crates/grida-canvas/src/svg/pack.rs index 13ad20f74c..809303c2a9 100644 --- a/crates/grida-canvas/src/svg/pack.rs +++ b/crates/grida-canvas/src/svg/pack.rs @@ -46,7 +46,7 @@ impl SceneBuilder { let root_id = self.graph.append_child(Node::Container(root), Parent::Root); for child in &scene.svg.children { - self.append_child(child, Parent::NodeId(root_id.clone()))?; + self.append_child(child, Parent::NodeId(root_id))?; } Ok(self.graph) @@ -72,7 +72,7 @@ impl SceneBuilder { node.transform = Some(group.transform.into()); let group_id = self.graph.append_child(Node::Group(node), parent); - let parent = Parent::NodeId(group_id.clone()); + let parent = Parent::NodeId(group_id); for child in &group.children { self.append_child(child, parent.clone())?; diff --git a/crates/grida-canvas/src/sys/clock.rs b/crates/grida-canvas/src/sys/clock.rs index 981fee8856..a697ad6350 100644 --- a/crates/grida-canvas/src/sys/clock.rs +++ b/crates/grida-canvas/src/sys/clock.rs @@ -103,6 +103,12 @@ impl Ticker for EventLoopClock { } } +impl Default for EventLoopClock { + fn default() -> Self { + Self::new() + } +} + impl EventLoopClock { /// Creates a new `EventLoopClock` initialized to the current time. /// diff --git a/crates/grida-canvas/src/sys/timer.rs b/crates/grida-canvas/src/sys/timer.rs index 36c625276e..f5fcc7ca5e 100644 --- a/crates/grida-canvas/src/sys/timer.rs +++ b/crates/grida-canvas/src/sys/timer.rs @@ -212,9 +212,9 @@ impl TimerMgr { last_execute_time: None, }; - Debounce { - state: std::sync::Arc::new(std::sync::Mutex::new(state)), - } + #[allow(clippy::arc_with_non_send_sync)] + let state = std::sync::Arc::new(std::sync::Mutex::new(state)); + Debounce { state } } /// Cancels a timer by its ID diff --git a/crates/grida-canvas/src/text/attributed_paragraph.rs b/crates/grida-canvas/src/text/attributed_paragraph.rs index b849464a90..b52383f85e 100644 --- a/crates/grida-canvas/src/text/attributed_paragraph.rs +++ b/crates/grida-canvas/src/text/attributed_paragraph.rs @@ -141,7 +141,7 @@ fn build_attributed_paragraph_inner( let has_any_strokes = attr .runs .iter() - .any(|r| r.strokes.as_ref().map_or(false, |s| !s.is_empty())); + .any(|r| r.strokes.as_ref().is_some_and(|s| !s.is_empty())); // ----- Fill paragraph ----- let fill_para = { diff --git a/crates/grida-canvas/src/text/paragraph_cache_layout.rs b/crates/grida-canvas/src/text/paragraph_cache_layout.rs index 8080da59ad..2a53829fc3 100644 --- a/crates/grida-canvas/src/text/paragraph_cache_layout.rs +++ b/crates/grida-canvas/src/text/paragraph_cache_layout.rs @@ -212,8 +212,7 @@ impl ParagraphCacheLayout { } } - let para = builder.build(); - para + builder.build() } /// Build and layout the paragraph with **per-run** attributed styling, diff --git a/crates/grida-canvas/src/text/text_style.rs b/crates/grida-canvas/src/text/text_style.rs index 16e758954e..016a96fc80 100644 --- a/crates/grida-canvas/src/text/text_style.rs +++ b/crates/grida-canvas/src/text/text_style.rs @@ -161,7 +161,7 @@ fn var_opsz(opsz: f32) -> skia_safe::font_arguments::variation_position::Coordin fn tag_from_str(tag: &str) -> skia_safe::FourByteTag { let bytes = tag.as_bytes(); - let b0 = *bytes.get(0).unwrap_or(&b' '); + let b0 = *bytes.first().unwrap_or(&b' '); let b1 = *bytes.get(1).unwrap_or(&b' '); let b2 = *bytes.get(2).unwrap_or(&b' '); let b3 = *bytes.get(3).unwrap_or(&b' '); diff --git a/crates/grida-canvas/src/text_edit/attributed_text/html.rs b/crates/grida-canvas/src/text_edit/attributed_text/html.rs index f7d98102d1..27f25f43ff 100644 --- a/crates/grida-canvas/src/text_edit/attributed_text/html.rs +++ b/crates/grida-canvas/src/text_edit/attributed_text/html.rs @@ -555,8 +555,7 @@ fn parse_css_color(val: &str) -> Option { } } - if val.starts_with('#') { - let hex = &val[1..]; + if let Some(hex) = val.strip_prefix('#') { if hex.len() == 6 { let r = u8::from_str_radix(&hex[0..2], 16).ok()?; let g = u8::from_str_radix(&hex[2..4], 16).ok()?; diff --git a/crates/grida-canvas/src/text_edit/attributed_text/mod.rs b/crates/grida-canvas/src/text_edit/attributed_text/mod.rs index 7ea0dbb332..91916cdef9 100644 --- a/crates/grida-canvas/src/text_edit/attributed_text/mod.rs +++ b/crates/grida-canvas/src/text_edit/attributed_text/mod.rs @@ -46,9 +46,10 @@ pub use crate::cg::types::{ /// A dimension that can be `Normal` (unset), a fixed px value, or a factor. /// /// Used for `line_height`, `letter_spacing`, `word_spacing`. -#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)] pub enum TextDimension { /// Normal / auto (no override). + #[default] Normal, /// Fixed value in layout-local points (px). Fixed(f32), @@ -56,12 +57,6 @@ pub enum TextDimension { Factor(f32), } -impl Default for TextDimension { - fn default() -> Self { - Self::Normal - } -} - /// Per-run stroke representation for text. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct TextStroke { @@ -71,19 +66,14 @@ pub struct TextStroke { } /// Paragraph direction. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)] pub enum ParagraphDirection { + #[default] Ltr, Rtl, Auto, } -impl Default for ParagraphDirection { - fn default() -> Self { - Self::Ltr - } -} - /// Hyperlink target on a text run. #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct Hyperlink { diff --git a/crates/grida-canvas/src/text_edit/mod.rs b/crates/grida-canvas/src/text_edit/mod.rs index 430f06627d..b79cef7f91 100644 --- a/crates/grida-canvas/src/text_edit/mod.rs +++ b/crates/grida-canvas/src/text_edit/mod.rs @@ -274,7 +274,7 @@ impl TextEditorState { } pub fn has_selection(&self) -> bool { - self.anchor.map_or(false, |a| a != self.cursor) + self.anchor.is_some_and(|a| a != self.cursor) } /// Whether the caret should be shown. @@ -983,7 +983,7 @@ pub fn apply_command_mut( ); debug_assert!( s.anchor - .map_or(true, |a| a <= s.text.len() && s.text.is_char_boundary(a)), + .is_none_or(|a| a <= s.text.len() && s.text.is_char_boundary(a)), "apply_command produced invalid anchor {:?} for text len {}", s.anchor, s.text.len(), diff --git a/crates/grida-canvas/src/text_edit/selection_rects.rs b/crates/grida-canvas/src/text_edit/selection_rects.rs index 52f419db3e..14ef06ebd8 100644 --- a/crates/grida-canvas/src/text_edit/selection_rects.rs +++ b/crates/grida-canvas/src/text_edit/selection_rects.rs @@ -21,23 +21,18 @@ use skia_safe::{ /// Controls how zero-width and empty-line selection rectangles are expanded. /// /// See the manifesto §"Selection geometry policy" for full semantics. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)] pub enum EmptyLineSelectionPolicy { /// Raw engine output, no expansion. Zero-width rects remain invisible. None, /// Expand zero-width rects by a small fixed amount (~0.5× font size). /// Non-empty lines keep glyph-tight bounds. + #[default] GlyphRect, /// Expand every selected line's rect to the full layout width. LineBox, } -impl Default for EmptyLineSelectionPolicy { - fn default() -> Self { - Self::GlyphRect - } -} - // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- diff --git a/crates/grida-canvas/src/text_edit/session.rs b/crates/grida-canvas/src/text_edit/session.rs index 32abab2a8d..6d0b770353 100644 --- a/crates/grida-canvas/src/text_edit/session.rs +++ b/crates/grida-canvas/src/text_edit/session.rs @@ -58,7 +58,7 @@ //! //! - [x] Copy/Cut — HTML + plain text (via [`selected_html`](TextEditSession::selected_html)) //! - [x] Paste — HTML with formatting, or plain text fallback -//! (via [`paste_attributed`](TextEditSession::paste_attributed)) +//! (via [`paste_attributed`](TextEditSession::paste_attributed)) //! //! ## Rendering (host responsibility, session provides data) //! @@ -71,9 +71,9 @@ //! ## History //! //! - [x] Undo/redo — snapshot-based with merge grouping -//! (consecutive typing, backspace, or delete are grouped; paste, -//! newline, and IME commit are discrete steps; style changes are -//! discrete steps; snapshots capture both text and style runs) +//! (consecutive typing, backspace, or delete are grouped; paste, +//! newline, and IME commit are discrete steps; style changes are +//! discrete steps; snapshots capture both text and style runs) //! //! ## Rich text (per-run styling via [`AttributedText`]) //! diff --git a/crates/grida-canvas/src/text_edit/skia_layout.rs b/crates/grida-canvas/src/text_edit/skia_layout.rs index 6ccf9af3d9..72a3392a04 100644 --- a/crates/grida-canvas/src/text_edit/skia_layout.rs +++ b/crates/grida-canvas/src/text_edit/skia_layout.rs @@ -107,11 +107,8 @@ fn attr_style_to_skia( } } - match style.letter_spacing { - at_mod::TextDimension::Fixed(v) => { - ts.set_letter_spacing(v); - } - _ => {} + if let at_mod::TextDimension::Fixed(v) = style.letter_spacing { + ts.set_letter_spacing(v); } ts.add_font_feature("kern", if style.font_kerning { 1 } else { 0 }); @@ -1403,8 +1400,8 @@ impl TextLayoutEngine for SkiaLayoutEngine { let u16_pos = utf8_to_utf16_offset(slice, local_offset) as u32; let range = block.paragraph.get_word_boundary(u16_pos); - let start = block.byte_start + utf16_to_utf8_offset(slice, range.start as usize); - let end = block.byte_start + utf16_to_utf8_offset(slice, range.end as usize); + let start = block.byte_start + utf16_to_utf8_offset(slice, range.start); + let end = block.byte_start + utf16_to_utf8_offset(slice, range.end); (start, end) } @@ -1433,7 +1430,7 @@ impl TextLayoutEngine for SkiaLayoutEngine { // Clamp selection to this block's range let sel_start = start.max(block.byte_start); let sel_end = end.min(block.byte_end); - if sel_start >= sel_end && !(block.byte_start == block.byte_end) { + if sel_start >= sel_end && (block.byte_start != block.byte_end) { continue; } @@ -1464,6 +1461,7 @@ impl TextLayoutEngine for SkiaLayoutEngine { let first_line = line_index_for_offset_utf8(&metrics, start); let last_line = line_index_for_offset_utf8(&metrics, end.saturating_sub(1).max(start)); + #[allow(clippy::needless_range_loop)] for idx in first_line..=last_line { let lm = &metrics[idx]; if !lm.is_empty_line(text) { diff --git a/crates/grida-canvas/src/vectornetwork/vn.rs b/crates/grida-canvas/src/vectornetwork/vn.rs index 2a3be65be1..e5b6d87815 100644 --- a/crates/grida-canvas/src/vectornetwork/vn.rs +++ b/crates/grida-canvas/src/vectornetwork/vn.rs @@ -257,7 +257,7 @@ impl PiecewiseVectorNetworkGeometry { /// - The segments list is flat; ordering and connectivity must be tracked /// separately to construct regions or faces. /// - Regions are optional and may be omitted for stroke-only paths. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct VectorNetwork { pub vertices: Vec<(f32, f32)>, pub segments: Vec, @@ -496,19 +496,9 @@ impl VectorNetwork { } } -impl Default for VectorNetwork { - fn default() -> Self { - VectorNetwork { - vertices: vec![], - segments: vec![], - regions: vec![], - } - } -} - -impl Into for VectorNetwork { - fn into(self) -> skia_safe::Path { - self.to_union_path() +impl From for skia_safe::Path { + fn from(val: VectorNetwork) -> Self { + val.to_union_path() } } diff --git a/crates/grida-canvas/src/vectornetwork/vn_painter.rs b/crates/grida-canvas/src/vectornetwork/vn_painter.rs index d671d53c5c..b6c15fe9fc 100644 --- a/crates/grida-canvas/src/vectornetwork/vn_painter.rs +++ b/crates/grida-canvas/src/vectornetwork/vn_painter.rs @@ -263,11 +263,9 @@ impl<'a> VNPainter<'a> { paint.set_style(PaintStyle::Fill); self.canvas.draw_path(path, &paint); } - } else { - if let Some(mut paint) = paint::sk_paint_stack_without_images(paints, size, true) { - paint.set_style(PaintStyle::Fill); - self.canvas.draw_path(path, &paint); - } + } else if let Some(mut paint) = paint::sk_paint_stack_without_images(paints, size, true) { + paint.set_style(PaintStyle::Fill); + self.canvas.draw_path(path, &paint); } } } diff --git a/crates/grida-canvas/src/window/application.rs b/crates/grida-canvas/src/window/application.rs index eeb740cbde..da1eccf662 100644 --- a/crates/grida-canvas/src/window/application.rs +++ b/crates/grida-canvas/src/window/application.rs @@ -44,6 +44,7 @@ pub enum BoundsTarget<'a> { impl<'a> BoundsTarget<'a> { /// Parse from a string, recognizing `""` as the scene target. + #[allow(clippy::should_implement_trait)] pub fn from_str(s: &'a str) -> Self { if s == "" { Self::Scene @@ -179,16 +180,11 @@ pub enum HostEvent { /// platform-specific event loop. pub type HostEventCallback = Arc; +#[derive(Default)] pub struct Clipboard { pub(crate) data: Option>, } -impl Default for Clipboard { - fn default() -> Self { - Self { data: None } - } -} - impl Clipboard { pub(crate) fn set_data(&mut self, data: Vec) { self.data = Some(data); @@ -710,7 +706,7 @@ impl ApplicationApi for UnknownTargetApplication { self.loaded_scenes = result .scene_ids .iter() - .zip(result.scenes.into_iter()) + .zip(result.scenes) .map(|(id, scene)| (id.clone(), scene)) .collect(); @@ -869,7 +865,7 @@ impl UnknownTargetApplication { &mut self, event: crate::surface::SurfaceEvent, ) -> crate::surface::SurfaceResponse { - let (hit_tester, response) = if let Some(scene) = self.renderer.scene.as_ref() { + let (_hit_tester, response) = if let Some(scene) = self.renderer.scene.as_ref() { let ht = crate::hittest::HitTester::with_graph(self.renderer.get_cache(), &scene.graph); let r = self .surface @@ -882,8 +878,6 @@ impl UnknownTargetApplication { .dispatch(event, &ht, &NoHierarchy, &self.ui_hit_regions); (ht, r) }; - drop(hit_tester); - if response.needs_redraw { self.queue(); } @@ -1490,7 +1484,6 @@ impl UnknownTargetApplication { } /// Hit test the current cursor position and store the result. - #[cfg(not(target_arch = "wasm32"))] pub(crate) fn resource_loaded(&mut self) { self.process_image_queue(); @@ -1612,17 +1605,17 @@ impl UnknownTargetApplication { let surface = self.state.surface_mut(); let canvas = surface.canvas(); if self.devtools_rendering_show_fps { - fps_overlay::FpsMeter::draw(&canvas, self.clock.hz() as f32); + fps_overlay::FpsMeter::draw(canvas, self.clock.hz() as f32); } if self.devtools_rendering_show_stats { if let Some(s) = self.last_stats.as_deref() { - stats_overlay::StatsOverlay::draw(&canvas, s, &self.clock); + stats_overlay::StatsOverlay::draw(canvas, s, &self.clock); } } if !self.highlight_strokes.is_empty() { stroke_overlay::StrokeOverlay::draw( - &canvas, + canvas, &self.highlight_strokes, &self.renderer.camera, self.renderer.get_cache(), @@ -1635,7 +1628,7 @@ impl UnknownTargetApplication { // zoom-independent caret width. if let Some(ref deco) = self.text_edit_decorations { crate::devtools::text_edit_decoration_overlay::TextEditDecorationOverlay::draw( - &canvas, + canvas, deco, &self.renderer.camera, self.renderer.get_cache(), @@ -1643,7 +1636,7 @@ impl UnknownTargetApplication { } // Surface interaction overlays (hover, selection, marquee) surface_overlay::SurfaceOverlay::draw( - &canvas, + canvas, &self.surface, &self.renderer.camera, self.renderer.get_cache(), @@ -1652,7 +1645,7 @@ impl UnknownTargetApplication { ); // Surface UI elements (size meter, frame titles, hit regions) crate::surface::ui::SurfaceUI::draw( - &canvas, + canvas, &self.surface, &self.renderer.camera, self.renderer.get_cache(), @@ -1662,7 +1655,7 @@ impl UnknownTargetApplication { &self.renderer.fonts, ); if self.devtools_rendering_show_ruler { - ruler_overlay::Ruler::draw(&canvas, &self.renderer.camera); + ruler_overlay::Ruler::draw(canvas, &self.renderer.camera); } if let Some(mut ctx) = surface.recording_context() { if let Some(mut direct) = ctx.as_direct_context() { diff --git a/crates/grida-dev/Cargo.toml b/crates/grida-dev/Cargo.toml index ca89639036..1f74d1cf06 100644 --- a/crates/grida-dev/Cargo.toml +++ b/crates/grida-dev/Cargo.toml @@ -50,3 +50,6 @@ rfd = "0.15" zip = { version = "8.2.0", default-features = false, features = [ "deflate-flate2", ] } + +[lints] +workspace = true diff --git a/crates/grida-dev/src/bench/runner.rs b/crates/grida-dev/src/bench/runner.rs index 8518232f73..ce3d4cfd3f 100644 --- a/crates/grida-dev/src/bench/runner.rs +++ b/crates/grida-dev/src/bench/runner.rs @@ -603,13 +603,6 @@ fn run_circle_pan_pass( ) } -/// Run a pan pass that interleaves settle (stable) frames at a fixed interval, -/// simulating the native viewer's settle countdown behavior. -/// -/// Every `settle_interval` interaction frames, a stable frame is inserted. -/// ALL frames (interaction + settle) go into the same stats, so p50/p95/p99/MAX -/// reflect what the user actually sees. `settle_us` holds the average cost of -/// settle frames specifically. // --------------------------------------------------------------------------- // Real-time event loop simulation // --------------------------------------------------------------------------- @@ -774,7 +767,7 @@ fn run_frameloop_pan_pass( next_scroll_ms += scroll_interval_ms; scroll_events_fired += 1; - if scroll_events_fired % 25 == 0 { + if scroll_events_fired.is_multiple_of(25) { pan_direction = -pan_direction; } } diff --git a/crates/grida-dev/src/reftest/render.rs b/crates/grida-dev/src/reftest/render.rs index e677084cff..7420066bb4 100644 --- a/crates/grida-dev/src/reftest/render.rs +++ b/crates/grida-dev/src/reftest/render.rs @@ -57,11 +57,11 @@ pub fn find_test_pairs_from_glob( ) -> Result> { // Determine the static (literal) root of the glob to compute correct relative paths let wc_idx = inputs_glob - .find(|c: char| matches!(c, '*' | '?' | '[')) + .find(['*', '?', '[']) .unwrap_or(inputs_glob.len()); let prefix = &inputs_glob[..wc_idx]; // Trim to directory boundary (drop any partial segment after the last separator) - let prefix_root = match prefix.rfind(|c: char| c == '/' || c == '\\') { + let prefix_root = match prefix.rfind(['/', '\\']) { Some(i) if i > 0 => &prefix[..i], Some(_) => "", None => prefix, @@ -182,7 +182,7 @@ pub fn render_svg_to_png( .iter() .filter_map(|id| geometry.get_render_bounds(id)) .reduce(|acc, rect| math2::rect::union(&[acc, rect])) - .unwrap_or_else(|| Rectangle { + .unwrap_or(Rectangle { x: 0.0, y: 0.0, width: 800.0, diff --git a/crates/grida-dev/src/reftest/runner.rs b/crates/grida-dev/src/reftest/runner.rs index 2d5f58793c..0daf074126 100644 --- a/crates/grida-dev/src/reftest/runner.rs +++ b/crates/grida-dev/src/reftest/runner.rs @@ -192,7 +192,7 @@ pub async fn run_reftest(args: &ReftestArgs) -> Result<()> { let mut test_results = Vec::new(); // Process each test sequentially - for (_index, pair) in test_pairs.iter().enumerate() { + for pair in test_pairs.iter() { pb.set_message(format!("processing {}", pair.test_name)); // Load reference PNG to get target dimensions for scaling diff --git a/crates/math2/Cargo.toml b/crates/math2/Cargo.toml index 9070e30a36..fa5a280d12 100644 --- a/crates/math2/Cargo.toml +++ b/crates/math2/Cargo.toml @@ -19,3 +19,6 @@ crate-type = ["rlib"] [dependencies] serde = { version = "1", features = ["derive"], optional = true } + +[lints] +workspace = true diff --git a/crates/math2/src/bezier.rs b/crates/math2/src/bezier.rs index b8f3ae7207..4d4cd08746 100644 --- a/crates/math2/src/bezier.rs +++ b/crates/math2/src/bezier.rs @@ -122,7 +122,7 @@ pub fn a2c( recursive: Option<(f32, f32, f32, f32)>, ) -> Vec { let pi = std::f32::consts::PI; - let _120 = pi * 120.0 / 180.0; + let angle_120_rad = pi * 120.0 / 180.0; let rad = pi / 180.0 * angle; let rotate = |x: f32, y: f32, r: f32| -> (f32, f32) { @@ -192,11 +192,11 @@ pub fn a2c( let mut df = f2 - f1; let mut res: Vec = Vec::new(); - if df.abs() > _120 { + if df.abs() > angle_120_rad { let f2old = f2; let x2old = x2; let y2old = y2; - f2 = f1 + _120 * if sweep_flag && f2 > f1 { 1.0 } else { -1.0 }; + f2 = f1 + angle_120_rad * if sweep_flag && f2 > f1 { 1.0 } else { -1.0 }; x2 = cx + rx * f2.cos(); y2 = cy + ry * f2.sin(); res = a2c( diff --git a/crates/math2/src/layout.rs b/crates/math2/src/layout.rs index 2be40b8d36..d5bc87f970 100644 --- a/crates/math2/src/layout.rs +++ b/crates/math2/src/layout.rs @@ -119,12 +119,10 @@ pub mod flex { } else { Axis::Y } + } else if width >= height { + Axis::X } else { - if width >= height { - Axis::X - } else { - Axis::Y - } + Axis::Y }; let gaps = rect::get_gaps(boundingboxes, axis); diff --git a/crates/math2/src/range.rs b/crates/math2/src/range.rs index 1edd53fa76..7545a01943 100644 --- a/crates/math2/src/range.rs +++ b/crates/math2/src/range.rs @@ -103,7 +103,7 @@ pub fn group_ranges_by_uniform_gap( loop_indices: sorted, min: starts.iter().cloned().fold(f32::INFINITY, f32::min), max: ends.iter().cloned().fold(f32::NEG_INFINITY, f32::max), - gap: distances.get(0).cloned().unwrap_or(0.0), + gap: distances.first().cloned().unwrap_or(0.0), }); } } diff --git a/crates/math2/src/raster.rs b/crates/math2/src/raster.rs index 174674127a..f1acc58639 100644 --- a/crates/math2/src/raster.rs +++ b/crates/math2/src/raster.rs @@ -38,7 +38,7 @@ pub fn fract(x: f32) -> f32 { /// assert!(v >= 0.0 && v <= 1.0); /// ``` pub fn noise(x: f32, y: f32) -> f32 { - fract(((x * 12.9898 + y * 78.233).sin()) * 43758.5453) + fract(((x * 12.9898 + y * 78.233).sin()) * 43_758.547) } /// Returns all integer pixel coordinates along a straight line between @@ -163,7 +163,7 @@ pub fn scale(bitmap: &Bitmap, factor: Vector2) -> Bitmap { /// Resizes a bitmap to the specified `[width, height]`. pub fn resize(bitmap: &Bitmap, dst: Vector2) -> Bitmap { - let (w2, h2) = (dst[0] as f32, dst[1] as f32); + let (w2, h2) = (dst[0], dst[1]); let fx = w2 / bitmap.width as f32; let fy = h2 / bitmap.height as f32; scale(bitmap, [fx, fy]) diff --git a/crates/math2/src/rect.rs b/crates/math2/src/rect.rs index ec5ed4148c..8189e02759 100644 --- a/crates/math2/src/rect.rs +++ b/crates/math2/src/rect.rs @@ -681,8 +681,8 @@ pub fn axis_projection_intersection( projections .iter() .skip(1) - .fold(Some(projections[0]), |acc, p| { - acc.and_then(|cur| super::vector2::intersection(cur, *p)) + .try_fold(projections[0], |acc, p| { + super::vector2::intersection(acc, *p) }) } diff --git a/crates/math2/src/snap.rs b/crates/math2/src/snap.rs index 62b463c284..e13a049dda 100644 --- a/crates/math2/src/snap.rs +++ b/crates/math2/src/snap.rs @@ -229,16 +229,16 @@ pub mod axis { for (i, &a) in agents.iter().enumerate() { let (_snap, delta, idxs) = align::scalar(a, anchors, threshold); let signed = _snap - a; - if delta.abs() <= threshold { - if min_delta.is_infinite() || (signed - signed_delta).abs() <= tolerance { - hit_agents.push(i); - for idx in idxs { - hit_anchors.insert(idx); - } - if delta.abs() < min_delta.abs() { - min_delta = delta; - signed_delta = signed; - } + if delta.abs() <= threshold + && (min_delta.is_infinite() || (signed - signed_delta).abs() <= tolerance) + { + hit_agents.push(i); + for idx in idxs { + hit_anchors.insert(idx); + } + if delta.abs() < min_delta.abs() { + min_delta = delta; + signed_delta = signed; } } } @@ -290,10 +290,10 @@ pub mod axis { let x = config .x - .and_then(|t| Some(snap1d(&x_agents, &x_anchors, t, tolerance))); + .map(|t| snap1d(&x_agents, &x_anchors, t, tolerance)); let y = config .y - .and_then(|t| Some(snap1d(&y_agents, &y_anchors, t, tolerance))); + .map(|t| snap1d(&y_agents, &y_anchors, t, tolerance)); Snap2DAxisAlignedResult { x, y } } @@ -384,7 +384,7 @@ pub mod canvas { let agent_points = rect::to_9points_chunk(&agent); let anchor_points: Vec = anchors .iter() - .flat_map(|r| rect::to_9points_chunk(r)) + .flat_map(rect::to_9points_chunk) .map(|p| (Some(p[0]), Some(p[1]))) .collect(); diff --git a/crates/math2/src/transform.rs b/crates/math2/src/transform.rs index aadd46c836..29cbde5b60 100644 --- a/crates/math2/src/transform.rs +++ b/crates/math2/src/transform.rs @@ -185,7 +185,7 @@ impl AffineTransform { let [[a, c, tx], [b, d, ty]] = self.matrix; let det = a * d - b * c; - if det.abs() < std::f32::EPSILON { + if det.abs() < f32::EPSILON { return None; } diff --git a/justfile b/justfile index 26b5f52177..a36525c15b 100644 --- a/justfile +++ b/justfile @@ -12,6 +12,7 @@ check: pnpm fmt:check cargo check --all-targets --all-features cargo fmt --all -- --check + cargo clippy --no-deps -- -D warnings # Run tests test: diff --git a/third_party/usvg/src/lib.rs b/third_party/usvg/src/lib.rs index d5e4ef7184..fa2e394099 100644 --- a/third_party/usvg/src/lib.rs +++ b/third_party/usvg/src/lib.rs @@ -52,6 +52,8 @@ and can focus just on the rendering part. #![warn(missing_docs)] #![warn(missing_debug_implementations)] #![warn(missing_copy_implementations)] +// Vendored third-party code — suppress clippy warnings +#![allow(clippy::all)] mod parser; #[cfg(feature = "text")] diff --git a/third_party/usvg/src/main.rs b/third_party/usvg/src/main.rs index 84ae50250e..794e2cf09f 100644 --- a/third_party/usvg/src/main.rs +++ b/third_party/usvg/src/main.rs @@ -1,6 +1,9 @@ // Copyright 2018 the Resvg Authors // SPDX-License-Identifier: Apache-2.0 OR MIT +// Vendored third-party code — suppress clippy warnings +#![allow(clippy::all)] + use std::fs::File; use std::io::{self, Read, Write}; use std::path::PathBuf;