use std::cmp;
use std::collections::HashSet;
use ratatui::{
prelude::{Alignment, Buffer, Constraint, Direction, Layout, Rect},
text::{Line, Span},
widgets::{ListState, StatefulWidget, Widget},
};
use crossterm::event::{MouseButton, MouseEventKind};
use smallvec::smallvec;
use crate::app::{Action, MouseArea};
use crate::config::Config;
use crate::device_kind::DeviceKind;
use crate::device_widget::DeviceWidget;
use crate::dropdown_widget::DropdownWidget;
use crate::node_widget::NodeWidget;
use crate::view::{self, ListKind, VolumeAdjustment};
use crate::wirehose::ObjectId;
#[derive(Default)]
pub struct ObjectList {
top: usize,
pub selected: Option<ObjectId>,
list_kind: ListKind,
device_kind: Option<DeviceKind>,
pub dropdown_state: ListState,
pub targets: Vec<(view::Target, String)>,
}
impl ObjectList {
pub fn new(list_kind: ListKind, device_kind: Option<DeviceKind>) -> Self {
Self {
top: 0,
selected: None,
list_kind,
device_kind,
..Default::default()
}
}
pub fn down(&mut self, view: &view::View) {
if self.dropdown_state.selected().is_some() {
self.dropdown_state.select_next();
} else {
let new_selected = view.next_id(self.list_kind, self.selected);
if new_selected.is_some() {
self.select(new_selected);
}
}
}
pub fn up(&mut self, view: &view::View) {
if self.dropdown_state.selected().is_some() {
self.dropdown_state.select_previous();
} else {
let new_selected = view.previous_id(self.list_kind, self.selected);
if new_selected.is_some() {
self.select(new_selected);
}
}
}
fn dropdown_open(&mut self, view: &view::View) {
let targets = match self.list_kind {
ListKind::Node(_) => self
.selected
.and_then(|object_id| view.node_targets(object_id)),
ListKind::Device => self
.selected
.and_then(|object_id| view.device_targets(object_id)),
};
if let Some((targets, index)) = targets {
if !targets.is_empty() {
self.targets = targets;
self.dropdown_state.select(Some(index));
}
}
}
fn selected_target(&self) -> Option<&view::Target> {
self.dropdown_state
.selected()
.and_then(|index| self.targets.get(index))
.map(|(target, _)| target)
}
pub fn dropdown_activate(&mut self, view: &view::View) {
if self.dropdown_state.selected().is_none() {
self.dropdown_open(view);
return;
}
if let (Some(object_id), Some(&target)) =
(self.selected, self.selected_target())
{
view.set_target(object_id, target);
};
self.dropdown_state.select(None);
}
pub fn dropdown_close(&mut self) {
self.dropdown_state.select(None);
}
pub fn set_target(&mut self, view: &view::View, target: view::Target) {
self.dropdown_state.select(None);
if let Some(object_id) = self.selected {
view.set_target(object_id, target);
};
}
pub fn toggle_mute(&mut self, view: &view::View) {
if matches!(self.list_kind, ListKind::Device) {
return;
}
if let Some(node_id) = self.selected {
view.mute(node_id);
}
}
pub fn set_absolute_volume(
&mut self,
view: &view::View,
volume: f32,
max: Option<f32>,
) -> bool {
if matches!(self.list_kind, ListKind::Device) {
return false;
}
if let Some(node_id) = self.selected {
return view.volume(
node_id,
VolumeAdjustment::Absolute(volume),
max,
);
}
false
}
pub fn set_relative_volume(
&mut self,
view: &view::View,
volume: f32,
max: Option<f32>,
) -> bool {
if matches!(self.list_kind, ListKind::Device) {
return false;
}
if let Some(node_id) = self.selected {
return view.volume(
node_id,
VolumeAdjustment::Relative(volume),
max,
);
}
false
}
pub fn set_default(&mut self, view: &view::View) {
if matches!(self.list_kind, ListKind::Device) {
return;
}
if let (Some(node_id), Some(device_kind)) =
(self.selected, self.device_kind)
{
view.set_default(node_id, device_kind);
}
}
fn selected_index(&self, view: &view::View) -> Option<usize> {
self.selected
.and_then(|selected| view.position(self.list_kind, selected))
}
fn select(&mut self, object_id: Option<ObjectId>) {
self.selected = object_id;
self.dropdown_close();
}
pub fn visible_objects(
&self,
area: &Rect,
view: &view::View,
) -> HashSet<ObjectId> {
let objects = view.object_ids(self.list_kind);
let last = cmp::min(objects.len(), self.top + self.visible_count(area));
let mut visible_objects = HashSet::from([ObjectId::from_raw_id(0)]);
for object_id in objects[self.top..last].iter().cloned() {
visible_objects.insert(object_id);
if let Some(node) = view.nodes.get(&object_id) {
visible_objects.extend(node.client_id);
visible_objects.extend(node.device_info.map(|(id, _, _)| id));
if let ListKind::Node(node_kind) = self.list_kind {
if let Some(target_id) = node
.target
.and_then(|target| target.resolve(view, node_kind))
{
visible_objects.insert(target_id);
if let Some(target_node) = view.nodes.get(&target_id) {
visible_objects.extend(target_node.client_id);
visible_objects.extend(
target_node.device_info.map(|(id, _, _)| id),
);
}
}
}
}
}
visible_objects
}
fn visible_count(&self, area: &Rect) -> usize {
let (_, list_area, _) = self.areas(area);
let full_height = match self.list_kind {
ListKind::Node(_) => {
NodeWidget::height().saturating_add(NodeWidget::spacing())
}
ListKind::Device => {
DeviceWidget::height().saturating_add(DeviceWidget::spacing())
}
};
(list_area.height / full_height) as usize
}
pub fn update(&mut self, area: Rect, view: &view::View) {
let selected_index = self.selected_index(view).or_else(|| {
self.select(view.next_id(self.list_kind, None));
self.selected_index(view)
});
let objects_len = view.len(self.list_kind);
let visible_count = self.visible_count(&area);
if self.top >= objects_len {
self.top = objects_len.saturating_sub(visible_count);
}
if self.selected.is_some() {
match selected_index {
Some(selected_index) => {
if selected_index >= self.top.saturating_add(visible_count)
{
let visible_count_except_last =
visible_count.saturating_sub(1);
self.top = selected_index
.saturating_sub(visible_count_except_last);
} else if selected_index < self.top {
self.top = selected_index;
}
}
None => self.select(None), }
}
}
fn areas(&self, area: &Rect) -> (Rect, Rect, Rect) {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1), Constraint::Min(0), Constraint::Length(1), ])
.split(*area);
(layout[0], layout[1], layout[2])
}
}
pub struct ObjectListWidget<'a, 'b> {
pub object_list: &'a mut ObjectList,
pub view: &'a view::View<'b>,
pub config: &'a Config,
}
struct ObjectListRenderContext<'a> {
list_area: Rect,
objects_layout: &'a [Rect],
objects_visible: usize,
}
impl ObjectListWidget<'_, '_> {
fn render_node_list(
&mut self,
node_kind: view::NodeKind,
context: ObjectListRenderContext,
area: Rect,
buf: &mut Buffer,
mouse_areas: &mut Vec<MouseArea>,
) {
let all_objects = self.view.full_nodes(node_kind);
let objects = all_objects
.iter()
.skip(self.object_list.top)
.take(context.objects_visible.saturating_add(1));
let objects_and_areas: Vec<(&&view::Node, &Rect)> =
objects.zip(context.objects_layout.iter()).collect();
for (object, &object_area) in &objects_and_areas {
let selected = self
.object_list
.selected
.map(|id| id == object.object_id)
.unwrap_or_default();
NodeWidget::new(
self.config,
self.object_list.device_kind,
object,
selected,
)
.render(object_area, buf, mouse_areas);
}
if self.object_list.dropdown_state.selected().is_some() {
if let Some((_, object_area)) =
objects_and_areas.iter().find(|(object, _)| {
self.object_list
.selected
.map(|id| id == object.object_id)
.unwrap_or_default()
})
{
DropdownWidget::new(
self.object_list,
&NodeWidget::dropdown_area(
self.object_list,
&context.list_area,
object_area,
),
self.config,
)
.render(area, buf, mouse_areas);
}
}
}
fn render_device_list(
&mut self,
context: ObjectListRenderContext,
area: Rect,
buf: &mut Buffer,
mouse_areas: &mut Vec<MouseArea>,
) {
let all_objects = self.view.full_devices();
let objects = all_objects
.iter()
.skip(self.object_list.top)
.take(context.objects_visible.saturating_add(1));
let objects_and_areas: Vec<(&&view::Device, &Rect)> =
objects.zip(context.objects_layout.iter()).collect();
for (object, &object_area) in &objects_and_areas {
let selected = self
.object_list
.selected
.map(|id| id == object.object_id)
.unwrap_or_default();
DeviceWidget::new(object, selected, self.config).render(
object_area,
buf,
mouse_areas,
);
}
if self.object_list.dropdown_state.selected().is_some() {
if let Some((_, object_area)) =
objects_and_areas.iter().find(|(object, _)| {
self.object_list
.selected
.map(|id| id == object.object_id)
.unwrap_or_default()
})
{
DropdownWidget::new(
self.object_list,
&DeviceWidget::dropdown_area(
self.object_list,
&context.list_area,
object_area,
),
self.config,
)
.render(area, buf, mouse_areas);
}
}
}
}
impl StatefulWidget for &mut ObjectListWidget<'_, '_> {
type State = Vec<MouseArea>;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let mouse_areas = state;
let (header_area, list_area, footer_area) =
self.object_list.areas(&area);
mouse_areas.push((
header_area,
smallvec![MouseEventKind::Down(MouseButton::Left)],
smallvec![Action::MoveUp],
));
mouse_areas.push((
footer_area,
smallvec![MouseEventKind::Down(MouseButton::Left)],
smallvec![Action::MoveDown],
));
mouse_areas.push((
list_area,
smallvec![MouseEventKind::ScrollUp],
smallvec![Action::MoveUp],
));
mouse_areas.push((
list_area,
smallvec![MouseEventKind::ScrollDown],
smallvec![Action::MoveDown],
));
let (spacing, height) = match self.object_list.list_kind {
ListKind::Node(_) => (NodeWidget::spacing(), NodeWidget::height()),
ListKind::Device => {
(DeviceWidget::spacing(), DeviceWidget::height())
}
};
let full_object_height = height.saturating_add(spacing);
let objects_visible = (list_area.height / full_object_height) as usize;
let len = self.view.len(self.object_list.list_kind);
if self.object_list.top > 0 {
Line::from(Span::styled(
&self.config.char_set.list_more,
self.config.theme.list_more,
))
.alignment(Alignment::Center)
.render(header_area, buf);
}
let is_bottom_last =
self.object_list.top.saturating_add(objects_visible)
== len.saturating_sub(1);
let is_bottom_enough =
(list_area.height % full_object_height) >= height;
if self.object_list.top.saturating_add(objects_visible) < len
&& !(is_bottom_last && is_bottom_enough)
{
Line::from(Span::styled(
&self.config.char_set.list_more,
self.config.theme.list_more,
))
.alignment(Alignment::Center)
.render(footer_area, buf);
}
let objects_layout = {
let object_height = height;
let mut constraints =
vec![Constraint::Length(object_height); objects_visible];
constraints.push(Constraint::Max(object_height));
let constraints = constraints;
Layout::default()
.direction(Direction::Vertical)
.constraints(constraints)
.spacing(spacing)
.split(list_area)
};
match self.object_list.list_kind {
ListKind::Node(node_kind) => {
self.render_node_list(
node_kind,
ObjectListRenderContext {
list_area,
objects_layout: &objects_layout,
objects_visible,
},
area,
buf,
mouse_areas,
);
}
ListKind::Device => {
self.render_device_list(
ObjectListRenderContext {
list_area,
objects_layout: &objects_layout,
objects_visible,
},
area,
buf,
mouse_areas,
);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config;
use crate::mock;
use crate::view::{ListKind, NodeKind, View};
use crate::wirehose::{state::State, PropertyStore, StateEvent};
use std::sync::Arc;
fn init() -> (State, mock::WirehoseHandle<'static>) {
let mut state = State::default();
let wirehose = mock::WirehoseHandle::default();
for i in 1..11 {
let object_id = ObjectId::from_raw_id(i);
let mut props = PropertyStore::default();
props.set_node_description(String::from("Test node"));
props.set_media_class(String::from("Stream/Output/Audio"));
props.set_media_name(String::from("Media name"));
props.set_node_name(String::from("Node name"));
props.set_object_serial(i as u64);
let props = props;
let events = vec![
StateEvent::NodeProperties { object_id, props },
StateEvent::NodePositions {
object_id,
positions: vec![0, 1],
},
StateEvent::NodeStreamStarted {
object_id,
rate: 44100,
peaks: Arc::new([0.0.into(), 0.0.into()]),
},
StateEvent::NodeVolumes {
object_id,
volumes: vec![0.0, 0.0],
},
StateEvent::NodeMute {
object_id,
mute: false,
},
];
for event in events {
state.update(event);
}
}
(state, wirehose)
}
fn create_node(
state: &mut State,
object_id: ObjectId,
media_class: &str,
node_name: &str,
) {
let mut props = PropertyStore::default();
props.set_node_description(String::from("Test node"));
props.set_media_class(String::from(media_class));
props.set_media_name(String::from("Media name"));
props.set_node_name(String::from(node_name));
props.set_object_serial(u32::from(object_id) as u64);
state.update(StateEvent::NodeProperties { object_id, props });
state.update(StateEvent::NodeVolumes {
object_id,
volumes: vec![1.0, 1.0],
});
state.update(StateEvent::NodeMute {
object_id,
mute: false,
});
}
#[test]
fn object_list_up_overflow() {
let (state, wirehose) = init();
let view = View::from(
&wirehose,
&state,
&config::Names::default(),
&Vec::new(),
);
let height = NodeWidget::height() + NodeWidget::spacing();
let rect = Rect::new(0, 0, 80, height * 3 + 2);
let mut object_list =
ObjectList::new(ListKind::Node(NodeKind::All), None);
object_list.down(&view);
assert_eq!(object_list.top, 0);
assert_eq!(object_list.selected, Some(ObjectId::from_raw_id(1)));
object_list.up(&view);
object_list.update(rect, &view);
assert_eq!(object_list.top, 0);
assert_eq!(object_list.selected, Some(ObjectId::from_raw_id(1)));
}
#[test]
fn object_list_down_overflow() {
let (state, wirehose) = init();
let view = View::from(
&wirehose,
&state,
&config::Names::default(),
&Vec::new(),
);
let height = NodeWidget::height() + NodeWidget::spacing();
let rect = Rect::new(0, 0, 80, height * 3 + 2);
let mut object_list =
ObjectList::new(ListKind::Node(NodeKind::All), None);
object_list.down(&view);
assert_eq!(object_list.top, 0);
assert_eq!(object_list.selected, Some(ObjectId::from_raw_id(1)));
let nodes_len = view.nodes.len();
for _ in 0..(nodes_len * 2) {
object_list.down(&view);
}
object_list.update(rect, &view);
assert_eq!(object_list.top, 7);
assert_eq!(object_list.selected, Some(ObjectId::from_raw_id(10)));
}
#[test]
fn visible_objects_changes_with_scroll() {
let (state, wirehose) = init();
let view = View::from(
&wirehose,
&state,
&config::Names::default(),
&Vec::new(),
);
let height = NodeWidget::height() + NodeWidget::spacing();
let rect = Rect::new(0, 0, 80, height * 3 + 2);
let mut object_list =
ObjectList::new(ListKind::Node(NodeKind::All), None);
let visible = object_list.visible_objects(&rect, &view);
assert_eq!(visible.len(), 4);
assert!(visible.contains(&ObjectId::from_raw_id(0)));
assert!(visible.contains(&ObjectId::from_raw_id(1)));
assert!(visible.contains(&ObjectId::from_raw_id(2)));
assert!(visible.contains(&ObjectId::from_raw_id(3)));
object_list.top = 5;
let visible = object_list.visible_objects(&rect, &view);
assert_eq!(visible.len(), 4);
assert!(visible.contains(&ObjectId::from_raw_id(0)));
assert!(visible.contains(&ObjectId::from_raw_id(6)));
assert!(visible.contains(&ObjectId::from_raw_id(7)));
assert!(visible.contains(&ObjectId::from_raw_id(8)));
object_list.top = 4;
let visible = object_list.visible_objects(&rect, &view);
assert_eq!(visible.len(), 4);
assert!(visible.contains(&ObjectId::from_raw_id(0)));
assert!(visible.contains(&ObjectId::from_raw_id(5)));
assert!(visible.contains(&ObjectId::from_raw_id(6)));
assert!(visible.contains(&ObjectId::from_raw_id(7)));
}
#[test]
fn visible_objects_includes_linked_clients() {
let (mut state, wirehose) = init();
let mut props = state
.nodes
.get(&ObjectId::from_raw_id(1))
.unwrap()
.props
.clone();
props.set_client_id(ObjectId::from_raw_id(101));
state.update(StateEvent::NodeProperties {
object_id: ObjectId::from_raw_id(1),
props,
});
let view = View::from(
&wirehose,
&state,
&config::Names::default(),
&Vec::new(),
);
let height = NodeWidget::height() + NodeWidget::spacing();
let rect = Rect::new(0, 0, 80, height + 2);
let object_list = ObjectList::new(ListKind::Node(NodeKind::All), None);
let visible = object_list.visible_objects(&rect, &view);
assert_eq!(visible.len(), 3);
assert!(visible.contains(&ObjectId::from_raw_id(0)));
assert!(visible.contains(&ObjectId::from_raw_id(1)));
assert!(visible.contains(&ObjectId::from_raw_id(101)));
}
#[test]
fn visible_objects_includes_linked_devices() {
let (mut state, wirehose) = init();
let mut props = state
.nodes
.get(&ObjectId::from_raw_id(1))
.unwrap()
.props
.clone();
props.set_device_id(ObjectId::from_raw_id(101));
let card_profile_device = 0;
props.set_card_profile_device(card_profile_device);
state.update(StateEvent::NodeProperties {
object_id: ObjectId::from_raw_id(1),
props,
});
state.update(StateEvent::DeviceProperties {
object_id: ObjectId::from_raw_id(101),
props: PropertyStore::default(),
});
state.update(StateEvent::DeviceProfile {
object_id: ObjectId::from_raw_id(101),
index: 1,
});
state.update(StateEvent::DeviceRoute {
object_id: ObjectId::from_raw_id(101),
index: 0,
device: card_profile_device,
profiles: vec![1],
description: String::new(),
available: true,
channel_volumes: vec![1.0],
mute: false,
});
let view = View::from(
&wirehose,
&state,
&config::Names::default(),
&Vec::new(),
);
let height = NodeWidget::height() + NodeWidget::spacing();
let rect = Rect::new(0, 0, 80, height + 2);
let object_list = ObjectList::new(ListKind::Node(NodeKind::All), None);
let visible = object_list.visible_objects(&rect, &view);
assert_eq!(visible.len(), 3);
assert!(visible.contains(&ObjectId::from_raw_id(0)));
assert!(visible.contains(&ObjectId::from_raw_id(1)));
assert!(visible.contains(&ObjectId::from_raw_id(101)));
}
#[test]
fn visible_objects_includes_target() {
let mut state = State::default();
let wirehose = mock::WirehoseHandle::default();
let stream_id = ObjectId::from_raw_id(0);
create_node(&mut state, stream_id, "Stream/Output/Audio", "stream");
let sink_id = ObjectId::from_raw_id(100);
create_node(&mut state, sink_id, "Audio/Sink", "sink");
state.update(StateEvent::Link {
object_id: ObjectId::from_raw_id(200),
output_id: stream_id,
input_id: sink_id,
});
let metadata_id = ObjectId::from_raw_id(300);
state.update(StateEvent::MetadataMetadataName {
object_id: metadata_id,
metadata_name: String::from("default"),
});
state.update(StateEvent::MetadataProperty {
object_id: metadata_id,
subject: u32::from(stream_id),
key: Some(String::from("target.node")),
value: Some(String::from("100")),
});
let view = View::from(
&wirehose,
&state,
&config::Names::default(),
&Vec::new(),
);
let height = NodeWidget::height() + NodeWidget::spacing();
let rect = Rect::new(0, 0, 80, height + 2);
let object_list =
ObjectList::new(ListKind::Node(NodeKind::Playback), None);
let visible = object_list.visible_objects(&rect, &view);
assert!(visible.contains(&stream_id));
assert!(visible.contains(&sink_id));
}
#[test]
fn visible_objects_includes_target_client() {
let mut state = State::default();
let wirehose = mock::WirehoseHandle::default();
let stream_id = ObjectId::from_raw_id(0);
create_node(&mut state, stream_id, "Stream/Output/Audio", "stream");
let sink_id = ObjectId::from_raw_id(100);
let sink_client_id = ObjectId::from_raw_id(101);
let mut props = PropertyStore::default();
props.set_node_description(String::from("Test sink"));
props.set_media_class(String::from("Audio/Sink"));
props.set_media_name(String::from("Media name"));
props.set_node_name(String::from("sink"));
props.set_object_serial(100);
props.set_client_id(sink_client_id);
state.update(StateEvent::NodeProperties {
object_id: sink_id,
props,
});
state.update(StateEvent::NodeVolumes {
object_id: sink_id,
volumes: vec![1.0, 1.0],
});
state.update(StateEvent::NodeMute {
object_id: sink_id,
mute: false,
});
state.update(StateEvent::Link {
object_id: ObjectId::from_raw_id(200),
output_id: stream_id,
input_id: sink_id,
});
let metadata_id = ObjectId::from_raw_id(300);
state.update(StateEvent::MetadataMetadataName {
object_id: metadata_id,
metadata_name: String::from("default"),
});
state.update(StateEvent::MetadataProperty {
object_id: metadata_id,
subject: u32::from(stream_id),
key: Some(String::from("target.node")),
value: Some(String::from("100")),
});
let view = View::from(
&wirehose,
&state,
&config::Names::default(),
&Vec::new(),
);
let height = NodeWidget::height() + NodeWidget::spacing();
let rect = Rect::new(0, 0, 80, height + 2);
let object_list =
ObjectList::new(ListKind::Node(NodeKind::Playback), None);
let visible = object_list.visible_objects(&rect, &view);
assert!(visible.contains(&stream_id));
assert!(visible.contains(&sink_id));
assert!(visible.contains(&sink_client_id));
}
#[test]
fn visible_objects_includes_target_device() {
let mut state = State::default();
let wirehose = mock::WirehoseHandle::default();
let stream_id = ObjectId::from_raw_id(0);
create_node(&mut state, stream_id, "Stream/Output/Audio", "stream");
let sink_id = ObjectId::from_raw_id(100);
let sink_device_id = ObjectId::from_raw_id(101);
let card_profile_device = 0;
let mut props = PropertyStore::default();
props.set_node_description(String::from("Test sink"));
props.set_media_class(String::from("Audio/Sink"));
props.set_media_name(String::from("Media name"));
props.set_node_name(String::from("sink"));
props.set_object_serial(100);
props.set_device_id(sink_device_id);
props.set_card_profile_device(card_profile_device);
state.update(StateEvent::NodeProperties {
object_id: sink_id,
props,
});
state.update(StateEvent::NodeVolumes {
object_id: sink_id,
volumes: vec![1.0, 1.0],
});
state.update(StateEvent::NodeMute {
object_id: sink_id,
mute: false,
});
state.update(StateEvent::DeviceProperties {
object_id: sink_device_id,
props: PropertyStore::default(),
});
state.update(StateEvent::DeviceProfile {
object_id: sink_device_id,
index: 1,
});
state.update(StateEvent::DeviceRoute {
object_id: sink_device_id,
index: 0,
device: card_profile_device,
profiles: vec![1],
description: String::new(),
available: true,
channel_volumes: vec![1.0],
mute: false,
});
state.update(StateEvent::Link {
object_id: ObjectId::from_raw_id(200),
output_id: stream_id,
input_id: sink_id,
});
let metadata_id = ObjectId::from_raw_id(300);
state.update(StateEvent::MetadataMetadataName {
object_id: metadata_id,
metadata_name: String::from("default"),
});
state.update(StateEvent::MetadataProperty {
object_id: metadata_id,
subject: u32::from(stream_id),
key: Some(String::from("target.node")),
value: Some(String::from("100")),
});
let view = View::from(
&wirehose,
&state,
&config::Names::default(),
&Vec::new(),
);
let height = NodeWidget::height() + NodeWidget::spacing();
let rect = Rect::new(0, 0, 80, height + 2);
let object_list =
ObjectList::new(ListKind::Node(NodeKind::Playback), None);
let visible = object_list.visible_objects(&rect, &view);
assert!(visible.contains(&stream_id));
assert!(visible.contains(&sink_id));
assert!(visible.contains(&sink_device_id));
}
#[test]
fn visible_objects_includes_default_sink() {
let mut state = State::default();
let wirehose = mock::WirehoseHandle::default();
let stream_id = ObjectId::from_raw_id(0);
create_node(&mut state, stream_id, "Stream/Output/Audio", "stream");
let sink_id = ObjectId::from_raw_id(100);
create_node(&mut state, sink_id, "Audio/Sink", "default_sink");
let metadata_id = ObjectId::from_raw_id(300);
state.update(StateEvent::MetadataMetadataName {
object_id: metadata_id,
metadata_name: String::from("default"),
});
state.update(StateEvent::MetadataProperty {
object_id: metadata_id,
subject: 0,
key: Some(String::from("default.audio.sink")),
value: Some(String::from("{\"name\":\"default_sink\"}")),
});
let view = View::from(
&wirehose,
&state,
&config::Names::default(),
&Vec::new(),
);
assert!(view.default_sink.is_some());
let height = NodeWidget::height() + NodeWidget::spacing();
let rect = Rect::new(0, 0, 80, height + 2);
let object_list =
ObjectList::new(ListKind::Node(NodeKind::Playback), None);
let visible = object_list.visible_objects(&rect, &view);
assert!(visible.contains(&stream_id));
assert!(visible.contains(&sink_id));
}
#[test]
fn visible_objects_includes_default_source() {
let mut state = State::default();
let wirehose = mock::WirehoseHandle::default();
let stream_id = ObjectId::from_raw_id(0);
create_node(&mut state, stream_id, "Stream/Input/Audio", "stream");
let source_id = ObjectId::from_raw_id(100);
create_node(&mut state, source_id, "Audio/Source", "default_source");
let metadata_id = ObjectId::from_raw_id(300);
state.update(StateEvent::MetadataMetadataName {
object_id: metadata_id,
metadata_name: String::from("default"),
});
state.update(StateEvent::MetadataProperty {
object_id: metadata_id,
subject: 0,
key: Some(String::from("default.audio.source")),
value: Some(String::from("{\"name\":\"default_source\"}")),
});
let view = View::from(
&wirehose,
&state,
&config::Names::default(),
&Vec::new(),
);
assert!(view.default_source.is_some());
let height = NodeWidget::height() + NodeWidget::spacing();
let rect = Rect::new(0, 0, 80, height + 2);
let object_list =
ObjectList::new(ListKind::Node(NodeKind::Recording), None);
let visible = object_list.visible_objects(&rect, &view);
assert!(visible.contains(&stream_id));
assert!(visible.contains(&source_id));
}
}