1use std::{
4 collections::{HashMap, HashSet},
5 ffi::c_void,
6 sync::{Arc, Mutex, RwLock, Weak},
7};
8
9use libobs::obs_scene_item;
10
11use crate::{
12 run_with_obs,
13 runtime::ObsRuntime,
14 unsafe_send::{NativeObjectId, Sendable, SmartPointerSendable},
15 utils::{ObsError, ObsString},
16};
17
18use super::{
19 scene_item::{
20 mark_scene_item_removed, scene_item_native_handle, wrap_owned_scene_item_ref,
21 ObsSceneItemHandle, SceneItemTraitSealed,
22 },
23 ObsSceneRef, SceneItemTrait, SceneItemsBySource,
24};
25
26pub(super) type SceneGroups = Arc<RwLock<Vec<Arc<ObsSceneGroupRef>>>>;
27
28#[derive(Debug, Clone)]
30pub struct ObsUngroupedItem {
31 previous_object_id: NativeObjectId,
32 item: Arc<dyn SceneItemTrait>,
33}
34
35impl ObsUngroupedItem {
36 pub fn previous_object_id(&self) -> NativeObjectId {
37 self.previous_object_id
38 }
39
40 pub fn item(&self) -> &Arc<dyn SceneItemTrait> {
41 &self.item
42 }
43
44 pub fn into_item(self) -> Arc<dyn SceneItemTrait> {
45 self.item
46 }
47}
48
49#[derive(Debug, Clone)]
55pub struct ObsSceneGroupRef {
56 name: ObsString,
57 scene_item_ptr: SmartPointerSendable<*mut obs_scene_item>,
58 removed: Arc<Mutex<bool>>,
59 operation_lock: Arc<Mutex<()>>,
60 runtime: ObsRuntime,
61 _scene_ptr: SmartPointerSendable<*mut libobs::obs_scene_t>,
62 items_by_source: SceneItemsBySource,
63 groups: Weak<RwLock<Vec<Arc<ObsSceneGroupRef>>>>,
64}
65
66impl ObsSceneGroupRef {
67 pub fn name(&self) -> &ObsString {
68 &self.name
69 }
70
71 pub fn add_item<T: SceneItemTrait + ?Sized>(&self, item: &T) -> Result<(), ObsError> {
74 let _operation = self
75 .operation_lock
76 .lock()
77 .map_err(|error| ObsError::LockError(error.to_string()))?;
78 self.runtime.ensure_same_runtime(&item.runtime())?;
79 if self.is_removed() || item.is_removed() {
80 return Err(ObsError::InvalidOperation(
81 "cannot group a removed scene item".into(),
82 ));
83 }
84 let group = self.scene_item_ptr.clone();
85 let item = scene_item_native_handle(item);
86 run_with_obs!(self.runtime, (group, item), move || unsafe {
87 if libobs::obs_sceneitem_is_group(item.get_ptr()) {
90 return Err(ObsError::InvalidOperation(
91 "nested OBS scene groups are not supported by this managed group API".into(),
92 ));
93 }
94 let parent = libobs::obs_sceneitem_get_scene(group.get_ptr());
95 let item_parent = libobs::obs_sceneitem_get_scene(item.get_ptr());
96 if parent.is_null() || item_parent != parent {
97 return Err(ObsError::InvalidOperation(
98 "scene item must be a top-level item in the same scene as the group".into(),
99 ));
100 }
101 libobs::obs_sceneitem_group_add_item(group.get_ptr(), item.get_ptr());
102 Ok(())
103 })?
104 }
105
106 pub fn remove_item<T: SceneItemTrait + ?Sized>(&self, item: &T) -> Result<(), ObsError> {
108 let _operation = self
109 .operation_lock
110 .lock()
111 .map_err(|error| ObsError::LockError(error.to_string()))?;
112 self.runtime.ensure_same_runtime(&item.runtime())?;
113 if self.is_removed() || item.is_removed() {
114 return Err(ObsError::InvalidOperation(
115 "cannot ungroup a removed scene item".into(),
116 ));
117 }
118 let group = self.scene_item_ptr.clone();
119 let item = scene_item_native_handle(item);
120 run_with_obs!(self.runtime, (group, item), move || unsafe {
121 let group_scene = libobs::obs_sceneitem_group_get_scene(group.get_ptr());
124 let item_parent = libobs::obs_sceneitem_get_scene(item.get_ptr());
125 if group_scene.is_null() || item_parent != group_scene {
126 return Err(ObsError::InvalidOperation(
127 "scene item is not a child of this group".into(),
128 ));
129 }
130 libobs::obs_sceneitem_group_remove_item(group.get_ptr(), item.get_ptr());
131 Ok(())
132 })?
133 }
134
135 pub fn items_in_order(&self) -> Result<Vec<Arc<dyn SceneItemTrait>>, ObsError> {
137 let _operation = self
138 .operation_lock
139 .lock()
140 .map_err(|error| ObsError::LockError(error.to_string()))?;
141 self.items_in_order_unlocked()
142 }
143
144 fn items_in_order_unlocked(&self) -> Result<Vec<Arc<dyn SceneItemTrait>>, ObsError> {
145 if self.is_removed() {
146 return Ok(Vec::new());
147 }
148 let group = self.scene_item_ptr.clone();
149 let addresses = run_with_obs!(self.runtime, (group), move || unsafe {
150 let mut result = Vec::<usize>::new();
151 libobs::obs_sceneitem_group_enum_items(
153 group.get_ptr(),
154 Some(collect_item_address),
155 (&mut result as *mut Vec<usize>).cast::<c_void>(),
156 );
157 result
158 })?;
159 map_native_order(
160 addresses,
161 &self.items_by_source,
162 self.groups.upgrade().as_ref(),
163 )
164 }
165
166 pub fn ungroup(&self) -> Result<Vec<ObsUngroupedItem>, ObsError> {
173 let _operation = self
174 .operation_lock
175 .lock()
176 .map_err(|error| ObsError::LockError(error.to_string()))?;
177 if self.is_removed() {
178 return Ok(Vec::new());
179 }
180
181 let old_children = self.items_in_order_unlocked()?;
182 let old_ids = old_children
183 .iter()
184 .map(|item| item.object_id())
185 .collect::<Vec<_>>();
186
187 let mut registry = self
190 .items_by_source
191 .write()
192 .map_err(|error| ObsError::LockError(error.to_string()))?;
193 for old_id in &old_ids {
194 let occurrences = registry
195 .values()
196 .flat_map(|(_, items)| items.iter())
197 .filter(|item| item.object_id() == *old_id)
198 .count();
199 if occurrences != 1 {
200 return Err(ObsError::InvalidOperation(format!(
201 "group child {old_id:?} has {occurrences} managed registry entries"
202 )));
203 }
204 }
205
206 let groups = self.groups.upgrade();
207 let mut groups_guard = match groups.as_ref() {
208 Some(groups) => Some(
209 groups
210 .write()
211 .map_err(|error| ObsError::LockError(error.to_string()))?,
212 ),
213 None => None,
214 };
215 if let Some(groups) = groups_guard.as_ref() {
216 if !groups
217 .iter()
218 .any(|group| group.object_id() == self.object_id())
219 {
220 return Err(ObsError::InvalidOperation(
221 "managed group is missing from its parent scene registry".into(),
222 ));
223 }
224 }
225
226 let before = native_scene_item_addresses(&self.runtime, self._scene_ptr.clone())?
227 .into_iter()
228 .collect::<HashSet<_>>();
229 let group = self.scene_item_ptr.clone();
230 let scene = self._scene_ptr.clone();
231 let new_native = run_with_obs!(self.runtime, (group, scene, before), move || unsafe {
232 libobs::obs_sceneitem_group_ungroup2(group.get_ptr(), true);
236
237 let mut after = Vec::<usize>::new();
238 libobs::obs_scene_enum_items(
239 scene.get_ptr(),
240 Some(collect_item_address),
241 (&mut after as *mut Vec<usize>).cast::<c_void>(),
242 );
243 let mut replacements = Vec::new();
244 for address in after {
245 if before.contains(&address) {
246 continue;
247 }
248 let item = address as *mut libobs::obs_sceneitem_t;
249 libobs::obs_sceneitem_addref(item);
250 replacements.push(Sendable(item));
251 }
252 replacements
253 })?;
254
255 let replacement_count = new_native.len();
256 let mut replacements = Vec::with_capacity(old_children.len().min(replacement_count));
257 let mut new_native = new_native.into_iter();
258
259 for old in &old_children {
260 let previous_object_id = old.object_id();
261 let Some(raw) = new_native.next() else {
262 mark_scene_item_removed(old.as_ref());
265 for (_, items) in registry.values_mut() {
266 items.retain(|item| item.object_id() != previous_object_id);
267 }
268 continue;
269 };
270
271 let replacement = Arc::new(ObsSceneItemHandle::from_owned_native_ref(
272 raw,
273 self._scene_ptr.clone(),
274 self.runtime.clone(),
275 ));
276 let replacement_dyn: Arc<dyn SceneItemTrait> = replacement;
277 for (_, items) in registry.values_mut() {
278 for slot in items.iter_mut() {
279 if slot.object_id() == previous_object_id {
280 *slot = replacement_dyn.clone();
281 }
282 }
283 }
284 mark_scene_item_removed(old.as_ref());
285 replacements.push(ObsUngroupedItem {
286 previous_object_id,
287 item: replacement_dyn,
288 });
289 }
290 registry.retain(|_, (_, items)| !items.is_empty());
291
292 let extras = new_native.collect::<Vec<_>>();
295 if !extras.is_empty() {
296 run_with_obs!(self.runtime, (extras), move || unsafe {
297 for item in extras {
299 libobs::obs_sceneitem_release(item.0);
300 }
301 })?;
302 }
303
304 {
305 let mut removed = self
306 .removed
307 .lock()
308 .unwrap_or_else(|poisoned| poisoned.into_inner());
309 *removed = true;
310 }
311 if let Some(groups) = groups_guard.as_mut() {
312 groups.retain(|group| group.object_id() != self.object_id());
313 }
314
315 if replacement_count != old_children.len() {
316 return Err(ObsError::Unexpected(format!(
317 "OBS ungroup replaced {} children with {replacement_count} native items",
318 old_children.len()
319 )));
320 }
321
322 Ok(replacements)
323 }
324}
325
326impl SceneItemTraitSealed for ObsSceneGroupRef {
327 fn __native_handle(&self) -> &SmartPointerSendable<*mut obs_scene_item> {
328 &self.scene_item_ptr
329 }
330
331 fn __removed_flag(&self) -> &Arc<Mutex<bool>> {
332 &self.removed
333 }
334}
335
336impl SceneItemTrait for ObsSceneGroupRef {
337 fn runtime(&self) -> ObsRuntime {
338 self.runtime.clone()
339 }
340
341 fn remove_from_scene(&self) -> Result<(), ObsError> {
342 let _operation = self
343 .operation_lock
344 .lock()
345 .map_err(|error| ObsError::LockError(error.to_string()))?;
346 if self.is_removed() {
347 return Ok(());
348 }
349 let children = self.items_in_order_unlocked()?;
350 let removed_ids = children
351 .iter()
352 .map(|child| child.object_id())
353 .collect::<HashSet<_>>();
354
355 let mut registry = self
359 .items_by_source
360 .write()
361 .map_err(|error| ObsError::LockError(error.to_string()))?;
362 for child_id in &removed_ids {
363 if !registry
364 .values()
365 .flat_map(|(_, items)| items.iter())
366 .any(|item| item.object_id() == *child_id)
367 {
368 return Err(ObsError::InvalidOperation(format!(
369 "group child {child_id:?} is missing from its parent scene registry"
370 )));
371 }
372 }
373 let groups = self.groups.upgrade();
374 let mut groups_guard = match groups.as_ref() {
375 Some(groups) => Some(
376 groups
377 .write()
378 .map_err(|error| ObsError::LockError(error.to_string()))?,
379 ),
380 None => None,
381 };
382
383 let group = self.scene_item_ptr.clone();
384 run_with_obs!(self.runtime, (group), move || unsafe {
385 libobs::obs_sceneitem_remove(group.get_ptr());
388 })?;
389
390 {
391 let mut removed = self
392 .removed
393 .lock()
394 .unwrap_or_else(|poisoned| poisoned.into_inner());
395 *removed = true;
396 }
397 for child in &children {
398 mark_scene_item_removed(child.as_ref());
399 }
400 registry.retain(|_, (_, items)| {
401 items.retain(|item| !removed_ids.contains(&item.object_id()));
402 !items.is_empty()
403 });
404 if let Some(groups) = groups_guard.as_mut() {
405 groups.retain(|group| group.object_id() != self.object_id());
406 }
407 Ok(())
408 }
409}
410
411impl ObsSceneRef {
412 pub fn create_group(
414 &mut self,
415 name: impl Into<ObsString>,
416 ) -> Result<ObsSceneGroupRef, ObsError> {
417 let name = name.into();
418 let scene = self.as_ptr();
419 let raw = run_with_obs!(self.runtime, (scene, name), move || unsafe {
420 let item = libobs::obs_scene_add_group(scene.get_ptr(), name.as_ptr().0);
423 if item.is_null() {
424 return Err(ObsError::NullPointer(Some(
425 "OBS scene group creation".into(),
426 )));
427 }
428 libobs::obs_sceneitem_addref(item);
429 Ok(Sendable(item))
430 })??;
431 let (scene_item_ptr, removed) = wrap_owned_scene_item_ref(raw, self.runtime.clone());
432 let group = ObsSceneGroupRef {
433 name: name.clone(),
434 scene_item_ptr,
435 removed,
436 operation_lock: Arc::new(Mutex::new(())),
437 runtime: self.runtime.clone(),
438 _scene_ptr: self.as_ptr(),
439 items_by_source: self.attached_scene_items.clone(),
440 groups: Arc::downgrade(&self.attached_groups),
441 };
442 self.attached_groups
443 .write()
444 .map_err(|error| ObsError::LockError(error.to_string()))?
445 .push(Arc::new(group.clone()));
446 Ok(group)
447 }
448
449 pub fn groups(&self) -> Result<Vec<ObsSceneGroupRef>, ObsError> {
451 Ok(self
452 .attached_groups
453 .read()
454 .map_err(|error| ObsError::LockError(error.to_string()))?
455 .iter()
456 .filter(|group| !group.is_removed())
457 .map(|group| (**group).clone())
458 .collect())
459 }
460
461 pub fn items_in_order(&self) -> Result<Vec<Arc<dyn SceneItemTrait>>, ObsError> {
464 let addresses = native_scene_item_addresses(&self.runtime, self.as_ptr())?;
465 map_native_order(
466 addresses,
467 &self.attached_scene_items,
468 Some(&self.attached_groups),
469 )
470 }
471
472 pub(super) fn clear_groups(&mut self) -> Result<(), ObsError> {
473 let groups = self
474 .attached_groups
475 .read()
476 .map_err(|error| ObsError::LockError(error.to_string()))?
477 .clone();
478 for group in groups {
479 group.remove_from_scene()?;
480 }
481 self.attached_groups
482 .write()
483 .map_err(|error| ObsError::LockError(error.to_string()))?
484 .clear();
485 Ok(())
486 }
487}
488
489fn native_scene_item_addresses(
490 runtime: &ObsRuntime,
491 scene: SmartPointerSendable<*mut libobs::obs_scene_t>,
492) -> Result<Vec<usize>, ObsError> {
493 run_with_obs!(runtime, (scene), move || unsafe {
494 let mut result = Vec::<usize>::new();
495 libobs::obs_scene_enum_items(
497 scene.get_ptr(),
498 Some(collect_item_address),
499 (&mut result as *mut Vec<usize>).cast::<c_void>(),
500 );
501 result
502 })
503}
504
505unsafe extern "C" fn collect_item_address(
509 _scene: *mut libobs::obs_scene_t,
510 item: *mut libobs::obs_sceneitem_t,
511 param: *mut c_void,
512) -> bool {
513 if let Some(result) = unsafe { param.cast::<Vec<usize>>().as_mut() } {
515 result.push(item as usize);
516 true
517 } else {
518 false
519 }
520}
521
522fn map_native_order(
523 addresses: Vec<usize>,
524 items_by_source: &SceneItemsBySource,
525 groups: Option<&SceneGroups>,
526) -> Result<Vec<Arc<dyn SceneItemTrait>>, ObsError> {
527 let mut managed = HashMap::<usize, Arc<dyn SceneItemTrait>>::new();
528 for (_, items) in items_by_source
529 .read()
530 .map_err(|error| ObsError::LockError(error.to_string()))?
531 .values()
532 {
533 for item in items {
534 if item.is_removed() {
535 continue;
536 }
537 let handle = scene_item_native_handle(item.as_ref());
538 managed.insert(unsafe { handle.raw_ptr_unchecked() } as usize, item.clone());
541 }
542 }
543 if let Some(groups) = groups {
544 for group in groups
545 .read()
546 .map_err(|error| ObsError::LockError(error.to_string()))?
547 .iter()
548 {
549 if group.is_removed() {
550 continue;
551 }
552 let handle = scene_item_native_handle(group.as_ref());
553 managed.insert(
556 unsafe { handle.raw_ptr_unchecked() } as usize,
557 group.clone() as Arc<dyn SceneItemTrait>,
558 );
559 }
560 }
561
562 let mut ordered = Vec::with_capacity(addresses.len());
563 for address in addresses {
564 let Some(item) = managed.get(&address) else {
565 return Err(ObsError::InvalidOperation(format!(
566 "scene contains unmanaged native item at 0x{address:x}"
567 )));
568 };
569 ordered.push(item.clone());
570 }
571 Ok(ordered)
572}