Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to inject custom events into application? #405

Closed
0x7CFE opened this issue Jun 13, 2020 · 7 comments · Fixed by #1198
Closed

How to inject custom events into application? #405

0x7CFE opened this issue Jun 13, 2020 · 7 comments · Fixed by #1198
Labels
question Further information is requested

Comments

@0x7CFE
Copy link
Contributor

0x7CFE commented Jun 13, 2020

Hello,

I have an app that needs to process MIDI events. They come from midir crate via a callback. The thing is, I don't quite understand, how to inject them into an application logic. Ideally I'd like them to be treated something like Message::MidiEvent(event) along with usual button presses and mouse events.

What's the best way to achieve that? If I'm not mistaken, it should be done by creating custom Recipe and subscribing to it. But maybe there is an easier way? In contrast to the download progress example, in my case it's just a plain event with no associated state. I just set up an input port and then receive callbacks on every MIDI event that I'd like to wrap into an application specific message.

Thank you.

@hecrj hecrj added the question Further information is requested label Jun 15, 2020
@hecrj
Copy link
Member

hecrj commented Jun 15, 2020

Yes, implementing a Recipe is the recommended way to run a task in the background and receive events. This will also allow you to cancel the subscription if you ever need to.

@nintha
Copy link

nintha commented Oct 31, 2020

@hecrj the Trait subscription::Recipe is not exported in the crate iced and iced_native. Has some way to implement a Recipe?

@Levitanus
Copy link

finally I've managed with Tick message. A bit dirty, but... works for me:

extern crate chrono;
extern crate iced;
extern crate midir;

use std::cell::RefCell;
use std::error::Error;
use std::io::{stdin, stdout, Write};
use std::option::Option::Some;
use std::sync::mpsc::{channel, Receiver, Sender};

use iced::{
    button, executor, pick_list, scrollable, time, Align, Application, Button, Column, Command,
    Container, Element, Length, PickList, Scrollable, Settings, Space, Subscription, Text,
};
use midir::{Ignore, MidiInput, MidiInputConnection, MidiInputPort, MidiInputPorts};

pub fn main() -> iced::Result {
    BigNote::run(Settings::default())
}

#[derive(Debug, Default, Clone, PartialEq, Eq)]
struct Port {
    pub index: usize,
    pub name: Box<String>,
}
impl std::fmt::Display for Port {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        // let midi_in = MidiInput::new("MyBigNote").unwrap();
        write!(
            f,
            "{}",
            // midi_in
            //     .port_name(midi_in.ports().get(self.index).unwrap())
            //     .unwrap()
            &self.name
        )
    }
}

#[derive(Debug, Clone)]
enum Message {
    PortSelected(Port),
    Tick(chrono::DateTime<chrono::Local>),
}

struct BigNote {
    note: i32,
    need_init: bool,
    client_name: String,
    scroll: scrollable::State,
    ports_list: pick_list::State<Port>,
    selected_port: Option<Port>,
    midi_in: MidiInput,

    connection: Option<MidiInputConnection<Sender<Vec<u8>>>>,
    reciever: Option<Receiver<Vec<u8>>>,
}

impl BigNote {
    fn ports_names(&self) -> Vec<Port> {
        let mut names: Vec<Port> = Vec::new();
        let midi_in = &self.midi_in;
        let ports_amount = midi_in.port_count();
        for port in 0..ports_amount {
            names.push(Port {
                index: port,
                name: Box::new(
                    midi_in
                        .port_name(midi_in.ports().get(port).unwrap())
                        .unwrap(),
                ),
            });
        }
        names.into()
    }
    fn connect(&mut self) {
        println!("init:");
        let ports = &self.midi_in.ports();
        let selected_port = self.selected_port.as_ref().unwrap();
        let port = ports.get(selected_port.index).unwrap();
        println!("in port name: {}", &self.midi_in.port_name(&port).unwrap());
        let (sender, reciever) = channel();
        self.reciever = Some(reciever);
        let midi_in = MidiInput::new(&self.client_name).unwrap();
        self.connection = Some(
            midi_in
                .connect(
                    &port,
                    "name",
                    |stamp, message, sender| {
                        // println!("{}: {:?} len = {}", stamp, message, message.len());
                        let mut packet = Vec::new();
                        packet.extend_from_slice(message);
                        sender.send(packet).unwrap();
                    },
                    sender,
                )
                .unwrap(),
        );
    }
}

impl Application for BigNote {
    type Executor = executor::Default;
    type Message = Message;
    type Flags = ();

    fn new(_flags: ()) -> (BigNote, Command<self::Message>) {
        (
            BigNote {
                note: -1,
                need_init: true,
                client_name: String::from("MyBigNote"),
                connection: Option::None,
                ports_list: pick_list::State::<Port>::default(),
                scroll: scrollable::State::default(),
                selected_port: None,
                midi_in: MidiInput::new("MyBigNote").unwrap(),
                reciever: None,
            },
            Command::none(),
        )
    }

    fn title(&self) -> String {
        String::from("MyBigNote by Levitanus")
    }

    fn view(&mut self) -> Element<Message> {
        let port_names = self.ports_names();
        let pick_list = PickList::new(
            &mut self.ports_list,
            port_names,
            self.selected_port.clone(),
            Message::PortSelected,
        );

        // let mut content = Scrollable::new(&mut self.scroll)
        //     .width(Length::Fill)
        //     .align_items(Align::Center)
        //     .spacing(10)
        //     .push(Space::with_height(Length::Units(600)))
        //     .push(Text::new("Which is your favorite String?"))
        //     .push(pick_list);

        // content = content.push(Space::with_height(Length::Units(600)));

        Container::new(pick_list)
            .width(Length::Fill)
            .height(Length::Fill)
            .center_x()
            .center_y()
            .into()
    }

    fn update(&mut self, message: Message) -> Command<Message> {
        match message {
            Message::PortSelected(port) => {
                println!("PortSelected");
                self.selected_port = Some(port);
                self.connect();
            }
            Message::Tick(_loc_time) => {
                if self.reciever.is_some() {
                    let reciever = &self.reciever.as_ref().unwrap();
                    loop {
                        let recv_result = reciever.try_recv();
                        if !recv_result.is_err() {
                            println!("{:?}", recv_result.as_ref().unwrap());
                        } else {
                            break;
                        }
                    }
                }
            }
        }
        Command::none()
    }

    fn subscription(&self) -> Subscription<Message> {
        let fps = 30;
        time::every(std::time::Duration::from_millis(1000 / fps))
            .map(|_| Message::Tick(chrono::Local::now()))
    }
}

@sourcebox
Copy link

I have exactly the same problem, I need to transfer data from a static callback to the application via a subscription.

The callback looks like this:

fn on_receive(_timestamp: u64, message: &[u8], _args: &mut ()) { println!("MIDI in {:?}", message); }

I had a look at the progress bar example, but I don't understand really how subscriptions work and how to implement a receipe for this. Is there a basic example that shows this use case without too much noise distracting from the real problem?

@Levitanus
Copy link

@sourcebox, code I've place above does exactly what You want. Maybe with not clean way with Recipe building, but generally It would work, handling all income midi messages in each visual frame.

By the advice of @Boddinagg I've used std::sync::mpsc::channel for transfer data from callback to the app.

@sourcebox
Copy link

@Levitanus I saw your code, but I really like to do it in a clean way.

@sourcebox
Copy link

Here's some progress on this: I managed to get it partially work, but not in a proper way.

fn subscription(&self) -> Subscription<Self::Message> {
    let tick_subscription =
        time::every(std::time::Duration::from_millis(1000)).map(|_| Message::Tick);

    let (sender, receiver) = mpsc::channel();
    self.midi.set_midi_in_sender(&sender);
    let midi_subscription =
        Subscription::from_recipe(MidiReceiveSubscription { receiver: receiver }).map(|data| {
            if data[0] == 0xB0 {
                Message::MidiCCReceived(data[0] & 0x0F, data[1], data[2])
            } else {
                Message::MidiUnknownReceived
            }
        });

    let subscriptions = vec![tick_subscription, midi_subscription];

    Subscription::batch(subscriptions.into_iter())
}

impl<H, I> iced_native::subscription::Recipe<H, I> for MidiReceiveSubscription
where
    H: Hasher,
{
    type Output = Vec<u8>;

    fn hash(&self, state: &mut H) {
        struct Marker;
        std::any::TypeId::of::<Marker>().hash(state);
    }

    fn stream(
        self: Box<Self>,
        _input: futures::stream::BoxStream<'static, I>,
    ) -> futures::stream::BoxStream<'static, Self::Output> {
        Box::pin(futures::stream::unfold(
            self.receiver,
            move |state| async move {
                let receiver = &state;
                let result = receiver.recv();
                if result.is_ok() {
                    Some((result.unwrap(), state))
                } else {
                    None
                }
            },
        ))
    }
}

The problem is that the mpsc channel is instanced inside the subscription function every time a new message is received. To my understanding, this is not the way to do it. But my attempts to change that all failed.

First subscription() takes &self as immutable, so there is no way to pass something in and out or set variables inside the application object. Second, the receipe does not take a reference to the mpsc receiver because it requires a static lifetime.

When the MIDI subscription is active, I'm not able to quit the application via Cmd-Q anymore, this is also something I need to solve.

@hecrj I would really appreciate any help on this topic. I think this whole thing is an easy task if you understand how it works, but I have to say, I still don't have a clue what's going on internally.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants