Skip to main content

pptx_rs/shape/
group.rs

1//! `Group`:组合形状。
2//!
3//! 组合是把多个形状"打包"为一个整体——移动、旋转、缩放时内部子形状按
4//! 相对位置一起变换。OOXML 用 `<p:grpSp>` 容器 + `<a:chOff>` / `<a:chExt>`
5//! 表达子坐标系。
6//!
7//! # 与 python-pptx 的对应
8//!
9//! - `pptx.shapes.group.Group` ←→ [`Group`];
10//! - `pptx.shapes.group.GroupShape`(与 `Shape` 同源)←→ [`GroupChild`]。
11//!
12//! # 坐标系
13//!
14//! Group 自身有 `off` / `ext` 表示"组合在父 slide 中的位置 + 尺寸";
15//! `chOff` / `chExt` 是子坐标系(默认与 off/ext 相同)。
16//! 修改 group 自身位置时同时 mutate `self.group.off`(参见 [`Shape::set_left`] 实现)。
17//!
18//! # 限制
19//!
20//! - 递归 Group(Group 嵌套 Group)已支持;
21//! - 子形状可读取,也可通过 `Group::add_child` / `Group::remove_child` 增删。
22
23use crate::oxml::shape::Group as OxmlGroup;
24use crate::oxml::shape::GroupChild as OxmlGroupChild;
25use crate::shape::autoshape::AutoShape;
26use crate::shape::base::Shape;
27use crate::shape::connector::Connector;
28use crate::shape::picture::Picture;
29use crate::shape::table::TableShape;
30use crate::units::Emu;
31
32/// 组合形状。
33#[derive(Clone, Debug, Default)]
34pub struct Group {
35    /// 内部 oxml 句柄。
36    pub(crate) group: OxmlGroup,
37}
38
39impl Group {
40    /// 从 oxml 构造。
41    pub fn from_group(g: OxmlGroup) -> Self {
42        Group { group: g }
43    }
44    /// 取出 oxml 引用。
45    pub fn group(&self) -> &OxmlGroup {
46        &self.group
47    }
48    /// 取出 oxml 可变引用。
49    pub fn group_mut(&mut self) -> &mut OxmlGroup {
50        &mut self.group
51    }
52
53    /// 取所有子形状(递归)。
54    pub fn children(&self) -> Vec<GroupChild> {
55        self.group
56            .children
57            .iter()
58            .map(|c| match c {
59                OxmlGroupChild::Sp(s) => GroupChild::Sp(AutoShape::from_sp(s.clone())),
60                OxmlGroupChild::Pic(p) => GroupChild::Pic(Picture::from_pic(p.clone())),
61                OxmlGroupChild::CxnSp(c) => GroupChild::Cx(Connector::from_cxn(c.clone())),
62                OxmlGroupChild::Group(g) => {
63                    GroupChild::Grp(Box::new(Group::from_group((**g).clone())))
64                }
65                // 图形框:目前仅支持 Table 高阶句柄,其它类型(chart/ole/smartArt)走默认 TableShape 占位。
66                // 读路径已通过 Graphic::SmartArt 保留原始 XML(TODO-037),但高阶层访问仍需通过 TableShape.frame.graphic。
67                OxmlGroupChild::GraphicFrame(g) => {
68                    GroupChild::Gfx(TableShape::from_frame(g.clone()))
69                }
70            })
71            .collect()
72    }
73
74    /// 子形状数量。
75    pub fn len(&self) -> usize {
76        self.group.children.len()
77    }
78    /// 是否无子形状。
79    pub fn is_empty(&self) -> bool {
80        self.group.children.is_empty()
81    }
82
83    // --------------------- 子形状编辑 API(TODO-032 高阶) ---------------------
84    //
85    // 对标 python-pptx 中通过 `group.shapes._spTree.append/remove` 操作子形状。
86    // 本库在 `Group` 上提供类型安全的 add/remove 接口,调用方传入高阶形状,
87    // 自动转换为底层 OxmlGroupChild 后追加。
88
89    /// 追加一个 [`AutoShape`] 子形状到组合末尾。
90    pub fn add_autoshape(&mut self, shape: AutoShape) -> &mut Self {
91        self.group
92            .children
93            .push(OxmlGroupChild::Sp(shape.sp.clone()));
94        self
95    }
96
97    /// 追加一个 [`Picture`] 子形状到组合末尾。
98    pub fn add_picture(&mut self, pic: Picture) -> &mut Self {
99        self.group
100            .children
101            .push(OxmlGroupChild::Pic(pic.pic.clone()));
102        self
103    }
104
105    /// 追加一个 [`Connector`] 子形状到组合末尾。
106    pub fn add_connector(&mut self, cxn: Connector) -> &mut Self {
107        self.group
108            .children
109            .push(OxmlGroupChild::CxnSp(cxn.cxn.clone()));
110        self
111    }
112
113    /// 追加一个 [`TableShape`](GraphicFrame)子形状到组合末尾。
114    pub fn add_table(&mut self, table: TableShape) -> &mut Self {
115        self.group
116            .children
117            .push(OxmlGroupChild::GraphicFrame(table.frame.clone()));
118        self
119    }
120
121    /// 追加一个嵌套 [`Group`] 子形状到组合末尾。
122    pub fn add_group(&mut self, grp: Group) -> &mut Self {
123        self.group
124            .children
125            .push(OxmlGroupChild::Group(Box::new(grp.group.clone())));
126        self
127    }
128
129    /// 按形状 ID 移除子形状。返回是否移除成功。
130    ///
131    /// 递归匹配:如果第一层未找到该 ID,会继续在嵌套 Group 中查找并移除。
132    pub fn remove_child(&mut self, id: u32) -> bool {
133        // 先在第一层找
134        let pos = self.group.children.iter().position(|c| match c {
135            OxmlGroupChild::Sp(s) => s.id == id,
136            OxmlGroupChild::Pic(p) => p.id == id,
137            OxmlGroupChild::CxnSp(c) => c.id == id,
138            OxmlGroupChild::Group(g) => g.id == id,
139            OxmlGroupChild::GraphicFrame(g) => g.id == id,
140        });
141        if let Some(p) = pos {
142            self.group.children.remove(p);
143            return true;
144        }
145        // 递归到嵌套 Group 中查找
146        for c in &mut self.group.children {
147            if let OxmlGroupChild::Group(g) = c {
148                let mut sub = Group::from_group((**g).clone());
149                if sub.remove_child(id) {
150                    **g = sub.group;
151                    return true;
152                }
153            }
154        }
155        false
156    }
157
158    /// 清空所有子形状。
159    pub fn clear(&mut self) {
160        self.group.children.clear();
161    }
162}
163
164/// 高阶 GroupChild 枚举。
165///
166/// 与 [`crate::oxml::shape::GroupChild`] 的区别在于:本枚举承载的是
167/// 高阶包装(`AutoShape` / `Picture` / `Connector` / `Group` / `TableShape`),
168/// 方便调用方继续操作。
169#[derive(Clone, Debug)]
170pub enum GroupChild {
171    /// 自选形状。
172    Sp(AutoShape),
173    /// 图片。
174    Pic(Picture),
175    /// 连接器。
176    Cx(Connector),
177    /// 递归 Group。
178    Grp(Box<Group>),
179    /// 图形框(目前仅承载表格)。
180    Gfx(TableShape),
181}
182
183impl Shape for Group {
184    fn id(&self) -> u32 {
185        self.group.id
186    }
187    fn set_id(&mut self, id: u32) {
188        self.group.id = id;
189    }
190    fn name(&self) -> &str {
191        &self.group.name
192    }
193    fn set_name(&mut self, name: String) {
194        self.group.name = name;
195    }
196    fn shape_type(&self) -> &'static str {
197        "group"
198    }
199
200    fn left(&self) -> Emu {
201        Emu::new(self.group.off.0.value())
202    }
203    fn set_left(&mut self, emu: Emu) {
204        self.group.off.0 = emu;
205    }
206    fn top(&self) -> Emu {
207        Emu::new(self.group.off.1.value())
208    }
209    fn set_top(&mut self, emu: Emu) {
210        self.group.off.1 = emu;
211    }
212    fn width(&self) -> Emu {
213        Emu::new(self.group.ext.0.value())
214    }
215    fn set_width(&mut self, emu: Emu) {
216        self.group.ext.0 = emu;
217    }
218    fn height(&self) -> Emu {
219        Emu::new(self.group.ext.1.value())
220    }
221    fn set_height(&mut self, emu: Emu) {
222        self.group.ext.1 = emu;
223    }
224
225    fn rotation(&self) -> f64 {
226        self.group.properties.rot_deg.unwrap_or(0.0)
227    }
228    fn set_rotation(&mut self, deg: f64) {
229        self.group.properties.rot_deg = Some(deg);
230        let rot = (deg * 60_000.0) as i32;
231        self.group.properties.xfrm.rot = Some(rot);
232    }
233}