|
@@ -151,6 +151,8 @@ pub(crate) struct Scheduler {
|
|
|
|
|
|
pub pending_tasks: VecDeque<UserEvent>,
|
|
|
|
|
|
+ pub batched_events: VecDeque<UserEvent>,
|
|
|
+
|
|
|
pub garbage_scopes: HashSet<ScopeId>,
|
|
|
|
|
|
pub lanes: [PriorityLane; 4],
|
|
@@ -214,7 +216,7 @@ impl Scheduler {
|
|
|
channel,
|
|
|
};
|
|
|
|
|
|
- let mut async_tasks = FuturesUnordered::new();
|
|
|
+ let async_tasks = FuturesUnordered::new();
|
|
|
|
|
|
Self {
|
|
|
pool,
|
|
@@ -233,6 +235,8 @@ impl Scheduler {
|
|
|
|
|
|
pending_tasks: VecDeque::new(),
|
|
|
|
|
|
+ batched_events: VecDeque::new(),
|
|
|
+
|
|
|
garbage_scopes: HashSet::new(),
|
|
|
|
|
|
current_priority: EventPriority::Low,
|
|
@@ -249,103 +253,52 @@ impl Scheduler {
|
|
|
|
|
|
pub fn manually_poll_events(&mut self) {
|
|
|
while let Ok(Some(msg)) = self.receiver.try_next() {
|
|
|
- self.handle_channel_msg(msg);
|
|
|
+ match msg {
|
|
|
+ SchedulerMsg::UiEvent(event) => self.ui_events.push_front(event),
|
|
|
+ SchedulerMsg::Immediate(im) => self.pending_immediates.push_front(im),
|
|
|
+
|
|
|
+ // task stuff
|
|
|
+ SchedulerMsg::SubmitTask(_, _) => todo!(),
|
|
|
+ SchedulerMsg::ToggleTask(_) => todo!(),
|
|
|
+ SchedulerMsg::PauseTask(_) => todo!(),
|
|
|
+ SchedulerMsg::ResumeTask(_) => todo!(),
|
|
|
+ SchedulerMsg::DropTask(_) => todo!(),
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // Converts UI events into dirty scopes with various priorities
|
|
|
- pub fn consume_pending_events(&mut self) {
|
|
|
- // consume all events that are "continuous" to be batched
|
|
|
- // if we run into a discrete event, then bail early
|
|
|
-
|
|
|
- while let Some(trigger) = self.ui_events.pop_back() {
|
|
|
- if let Some(scope) = self.pool.get_scope_mut(trigger.scope) {
|
|
|
- if let Some(element) = trigger.mounted_dom_id {
|
|
|
- let priority = match trigger.name {
|
|
|
- // clipboard
|
|
|
- "copy" | "cut" | "paste" => EventPriority::Medium,
|
|
|
-
|
|
|
- // Composition
|
|
|
- "compositionend" | "compositionstart" | "compositionupdate" => {
|
|
|
- EventPriority::Low
|
|
|
- }
|
|
|
-
|
|
|
- // Keyboard
|
|
|
- "keydown" | "keypress" | "keyup" => EventPriority::Low,
|
|
|
-
|
|
|
- // Focus
|
|
|
- "focus" | "blur" => EventPriority::Low,
|
|
|
-
|
|
|
- // Form
|
|
|
- "change" | "input" | "invalid" | "reset" | "submit" => EventPriority::Low,
|
|
|
-
|
|
|
- // Mouse
|
|
|
- "click" | "contextmenu" | "doubleclick" | "drag" | "dragend"
|
|
|
- | "dragenter" | "dragexit" | "dragleave" | "dragover" | "dragstart"
|
|
|
- | "drop" | "mousedown" | "mouseenter" | "mouseleave" | "mousemove"
|
|
|
- | "mouseout" | "mouseover" | "mouseup" => EventPriority::Low,
|
|
|
-
|
|
|
- "mousemove" => EventPriority::Medium,
|
|
|
-
|
|
|
- // Pointer
|
|
|
- "pointerdown" | "pointermove" | "pointerup" | "pointercancel"
|
|
|
- | "gotpointercapture" | "lostpointercapture" | "pointerenter"
|
|
|
- | "pointerleave" | "pointerover" | "pointerout" => EventPriority::Low,
|
|
|
-
|
|
|
- // Selection
|
|
|
- "select" | "touchcancel" | "touchend" => EventPriority::Low,
|
|
|
-
|
|
|
- // Touch
|
|
|
- "touchmove" | "touchstart" => EventPriority::Low,
|
|
|
+ // returns true if the event is discrete
|
|
|
+ pub fn handle_ui_event(&mut self, event: UserEvent) -> bool {
|
|
|
+ let (discrete, priority) = event_meta(&event);
|
|
|
|
|
|
- // Wheel
|
|
|
- "scroll" | "wheel" => EventPriority::Low,
|
|
|
+ if let Some(scope) = self.pool.get_scope_mut(event.scope) {
|
|
|
+ if let Some(element) = event.mounted_dom_id {
|
|
|
+ // TODO: bubble properly here
|
|
|
+ scope.call_listener(event.event, element);
|
|
|
|
|
|
- // Media
|
|
|
- "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied"
|
|
|
- | "encrypted" | "ended" | "error" | "loadeddata" | "loadedmetadata"
|
|
|
- | "loadstart" | "pause" | "play" | "playing" | "progress"
|
|
|
- | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend"
|
|
|
- | "timeupdate" | "volumechange" | "waiting" => EventPriority::Low,
|
|
|
-
|
|
|
- // Animation
|
|
|
- "animationstart" | "animationend" | "animationiteration" => {
|
|
|
- EventPriority::Low
|
|
|
- }
|
|
|
-
|
|
|
- // Transition
|
|
|
- "transitionend" => EventPriority::Low,
|
|
|
-
|
|
|
- // Toggle
|
|
|
- "toggle" => EventPriority::Low,
|
|
|
-
|
|
|
- _ => EventPriority::Low,
|
|
|
- };
|
|
|
-
|
|
|
- scope.call_listener(trigger.event, element);
|
|
|
- // let receiver = self.immediate_receiver.clone();
|
|
|
- // let mut receiver = receiver.borrow_mut();
|
|
|
-
|
|
|
- // // Drain the immediates into the dirty scopes, setting the appropiate priorities
|
|
|
- // while let Ok(Some(dirty_scope)) = receiver.try_next() {
|
|
|
+ while let Ok(Some(dirty_scope)) = self.receiver.try_next() {
|
|
|
+ //
|
|
|
// self.add_dirty_scope(dirty_scope, trigger.priority)
|
|
|
- // }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- // nothing to do, no events on channels, no work
|
|
|
- pub fn has_any_work(&self) -> bool {
|
|
|
- let pending_lanes = self.lanes.iter().find(|f| f.has_work()).is_some();
|
|
|
- pending_lanes || self.has_pending_events()
|
|
|
- }
|
|
|
+ use EventPriority::*;
|
|
|
|
|
|
- pub fn has_pending_events(&self) -> bool {
|
|
|
- self.ui_events.len() > 0
|
|
|
+ match priority {
|
|
|
+ Immediate => todo!(),
|
|
|
+ High => todo!(),
|
|
|
+ Medium => todo!(),
|
|
|
+ Low => todo!(),
|
|
|
+ }
|
|
|
+
|
|
|
+ discrete
|
|
|
}
|
|
|
|
|
|
- fn shift_priorities(&mut self) {
|
|
|
+ fn prepare_work(&mut self) {
|
|
|
+ // consume all events that are "continuous" to be batched
|
|
|
+ // if we run into a discrete event, then bail early
|
|
|
+
|
|
|
self.current_priority = match (
|
|
|
self.lanes[0].has_work(),
|
|
|
self.lanes[1].has_work(),
|
|
@@ -355,8 +308,26 @@ impl Scheduler {
|
|
|
(true, _, _, _) => EventPriority::Immediate,
|
|
|
(false, true, _, _) => EventPriority::High,
|
|
|
(false, false, true, _) => EventPriority::Medium,
|
|
|
- (false, false, false, _) => EventPriority::Low,
|
|
|
+ (false, false, false, true) => EventPriority::Low,
|
|
|
+ (false, false, false, false) => {
|
|
|
+ // no work to do, process events
|
|
|
+ EventPriority::Low
|
|
|
+ }
|
|
|
};
|
|
|
+
|
|
|
+ while let Some(trigger) = self.ui_events.pop_back() {
|
|
|
+ if let Some(scope) = self.pool.get_scope_mut(trigger.scope) {}
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // nothing to do, no events on channels, no work
|
|
|
+ pub fn has_any_work(&self) -> bool {
|
|
|
+ let pending_lanes = self.lanes.iter().find(|f| f.has_work()).is_some();
|
|
|
+ pending_lanes || self.has_pending_events()
|
|
|
+ }
|
|
|
+
|
|
|
+ pub fn has_pending_events(&self) -> bool {
|
|
|
+ self.ui_events.len() > 0
|
|
|
}
|
|
|
|
|
|
/// re-balance the work lanes, ensuring high-priority work properly bumps away low priority work
|
|
@@ -385,26 +356,97 @@ impl Scheduler {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /// Work the scheduler down, not polling any ongoing tasks.
|
|
|
- ///
|
|
|
- /// Will use the standard priority-based scheduling, batching, etc, but just won't interact with the async reactor.
|
|
|
- pub fn work_sync<'a>(&'a mut self) -> Vec<Mutations<'a>> {
|
|
|
- let mut committed_mutations = Vec::new();
|
|
|
+ pub fn current_lane(&mut self) -> &mut PriorityLane {
|
|
|
+ match self.current_priority {
|
|
|
+ EventPriority::Immediate => &mut self.lanes[0],
|
|
|
+ EventPriority::High => &mut self.lanes[1],
|
|
|
+ EventPriority::Medium => &mut self.lanes[2],
|
|
|
+ EventPriority::Low => &mut self.lanes[3],
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- self.manually_poll_events();
|
|
|
+ pub fn handle_channel_msg(&mut self, msg: SchedulerMsg) {
|
|
|
+ match msg {
|
|
|
+ SchedulerMsg::Immediate(_) => todo!(),
|
|
|
|
|
|
- if !self.has_any_work() {
|
|
|
- return committed_mutations;
|
|
|
+ SchedulerMsg::UiEvent(event) => {
|
|
|
+ //
|
|
|
+
|
|
|
+ self.handle_ui_event(event);
|
|
|
+ }
|
|
|
+
|
|
|
+ //
|
|
|
+ SchedulerMsg::SubmitTask(_, _) => todo!(),
|
|
|
+ SchedulerMsg::ToggleTask(_) => todo!(),
|
|
|
+ SchedulerMsg::PauseTask(_) => todo!(),
|
|
|
+ SchedulerMsg::ResumeTask(_) => todo!(),
|
|
|
+ SchedulerMsg::DropTask(_) => todo!(),
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- self.consume_pending_events();
|
|
|
+ fn add_dirty_scope(&mut self, scope: ScopeId, priority: EventPriority) {
|
|
|
+ todo!()
|
|
|
+ // match priority {
|
|
|
+ // EventPriority::High => self.high_priorty.dirty_scopes.insert(scope),
|
|
|
+ // EventPriority::Medium => self.medium_priority.dirty_scopes.insert(scope),
|
|
|
+ // EventPriority::Low => self.low_priority.dirty_scopes.insert(scope),
|
|
|
+ // };
|
|
|
+ }
|
|
|
|
|
|
- while self.has_any_work() {
|
|
|
- self.shift_priorities();
|
|
|
- self.work_on_current_lane(|| false, &mut committed_mutations);
|
|
|
+ async fn wait_for_any_work(&mut self) {}
|
|
|
+
|
|
|
+ /// Load the current lane, and work on it, periodically checking in if the deadline has been reached.
|
|
|
+ ///
|
|
|
+ /// Returns true if the lane is finished before the deadline could be met.
|
|
|
+ pub fn work_on_current_lane(
|
|
|
+ &mut self,
|
|
|
+ deadline_reached: impl FnMut() -> bool,
|
|
|
+ mutations: &mut Vec<Mutations>,
|
|
|
+ ) -> bool {
|
|
|
+ // Work through the current subtree, and commit the results when it finishes
|
|
|
+ // When the deadline expires, give back the work
|
|
|
+ let saved_state = self.load_work();
|
|
|
+
|
|
|
+ // We have to split away some parts of ourself - current lane is borrowed mutably
|
|
|
+ let mut shared = self.pool.clone();
|
|
|
+ let mut machine = unsafe { saved_state.promote(&mut shared) };
|
|
|
+
|
|
|
+ if machine.stack.is_empty() {
|
|
|
+ let shared = self.pool.clone();
|
|
|
+
|
|
|
+ self.current_lane().dirty_scopes.sort_by(|a, b| {
|
|
|
+ let h1 = shared.get_scope(*a).unwrap().height;
|
|
|
+ let h2 = shared.get_scope(*b).unwrap().height;
|
|
|
+ h1.cmp(&h2)
|
|
|
+ });
|
|
|
+
|
|
|
+ if let Some(scope) = self.current_lane().dirty_scopes.pop() {
|
|
|
+ let component = self.pool.get_scope(scope).unwrap();
|
|
|
+ let (old, new) = (component.frames.wip_head(), component.frames.fin_head());
|
|
|
+ machine.stack.push(DiffInstruction::Diff { new, old });
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- committed_mutations
|
|
|
+ let deadline_expired = machine.work(deadline_reached);
|
|
|
+
|
|
|
+ let machine: DiffMachine<'static> = unsafe { std::mem::transmute(machine) };
|
|
|
+ let mut saved = machine.save();
|
|
|
+
|
|
|
+ if deadline_expired {
|
|
|
+ self.save_work(saved);
|
|
|
+ false
|
|
|
+ } else {
|
|
|
+ for node in saved.seen_scopes.drain() {
|
|
|
+ self.current_lane().dirty_scopes.remove(&node);
|
|
|
+ }
|
|
|
+
|
|
|
+ let mut new_mutations = Mutations::new();
|
|
|
+ std::mem::swap(&mut new_mutations, &mut saved.mutations);
|
|
|
+
|
|
|
+ mutations.push(new_mutations);
|
|
|
+ self.save_work(saved);
|
|
|
+ true
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/// The primary workhorse of the VirtualDOM.
|
|
@@ -449,24 +491,25 @@ impl Scheduler {
|
|
|
pin_mut!(deadline);
|
|
|
|
|
|
loop {
|
|
|
- // Internalize any pending work since the last time we ran
|
|
|
+ // Move work out of the scheduler message receiver and into dedicated message lanes
|
|
|
self.manually_poll_events();
|
|
|
|
|
|
// Wait for any new events if we have nothing to do
|
|
|
// todo: poll the events once even if there is work to do to prevent starvation
|
|
|
if !self.has_any_work() {
|
|
|
futures_util::select! {
|
|
|
- msg = self.async_tasks.next() => {}
|
|
|
- msg = self.receiver.next() => self.handle_channel_msg(msg.unwrap()),
|
|
|
+ // todo: find a solution for the exhausted queue problem
|
|
|
+ // msg = self.async_tasks.next() => {}
|
|
|
+ msg = self.receiver.next() => {
|
|
|
+ log::debug!("received work in scheduler");
|
|
|
+ self.handle_channel_msg(msg.unwrap())
|
|
|
+ },
|
|
|
_ = (&mut deadline).fuse() => return committed_mutations,
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // Create work from the pending event queue
|
|
|
- self.consume_pending_events();
|
|
|
-
|
|
|
- // shift to the correct lane
|
|
|
- self.shift_priorities();
|
|
|
+ // switch our priority, pop off any work
|
|
|
+ self.prepare_work();
|
|
|
|
|
|
let mut deadline_reached = || (&mut deadline).now_or_never().is_some();
|
|
|
|
|
@@ -481,94 +524,30 @@ impl Scheduler {
|
|
|
committed_mutations
|
|
|
}
|
|
|
|
|
|
- /// Load the current lane, and work on it, periodically checking in if the deadline has been reached.
|
|
|
+ /// Work the scheduler down, not polling any ongoing tasks.
|
|
|
///
|
|
|
- /// Returns true if the lane is finished before the deadline could be met.
|
|
|
- pub fn work_on_current_lane(
|
|
|
- &mut self,
|
|
|
- deadline_reached: impl FnMut() -> bool,
|
|
|
- mutations: &mut Vec<Mutations>,
|
|
|
- ) -> bool {
|
|
|
- // Work through the current subtree, and commit the results when it finishes
|
|
|
- // When the deadline expires, give back the work
|
|
|
- let saved_state = self.load_work();
|
|
|
-
|
|
|
- // We have to split away some parts of ourself - current lane is borrowed mutably
|
|
|
- let mut shared = self.pool.clone();
|
|
|
- let mut machine = unsafe { saved_state.promote(&mut shared) };
|
|
|
-
|
|
|
- if machine.stack.is_empty() {
|
|
|
- let shared = self.pool.clone();
|
|
|
-
|
|
|
- self.current_lane().dirty_scopes.sort_by(|a, b| {
|
|
|
- let h1 = shared.get_scope(*a).unwrap().height;
|
|
|
- let h2 = shared.get_scope(*b).unwrap().height;
|
|
|
- h1.cmp(&h2)
|
|
|
- });
|
|
|
-
|
|
|
- if let Some(scope) = self.current_lane().dirty_scopes.pop() {
|
|
|
- let component = self.pool.get_scope(scope).unwrap();
|
|
|
- let (old, new) = (component.frames.wip_head(), component.frames.fin_head());
|
|
|
- machine.stack.push(DiffInstruction::Diff { new, old });
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- let deadline_expired = machine.work(deadline_reached);
|
|
|
-
|
|
|
- let machine: DiffMachine<'static> = unsafe { std::mem::transmute(machine) };
|
|
|
- let mut saved = machine.save();
|
|
|
-
|
|
|
- if deadline_expired {
|
|
|
- self.save_work(saved);
|
|
|
- false
|
|
|
- } else {
|
|
|
- for node in saved.seen_scopes.drain() {
|
|
|
- self.current_lane().dirty_scopes.remove(&node);
|
|
|
- }
|
|
|
-
|
|
|
- let mut new_mutations = Mutations::new();
|
|
|
- std::mem::swap(&mut new_mutations, &mut saved.mutations);
|
|
|
+ /// Will use the standard priority-based scheduling, batching, etc, but just won't interact with the async reactor.
|
|
|
+ pub fn work_sync<'a>(&'a mut self) -> Vec<Mutations<'a>> {
|
|
|
+ let mut committed_mutations = Vec::new();
|
|
|
|
|
|
- mutations.push(new_mutations);
|
|
|
- self.save_work(saved);
|
|
|
- true
|
|
|
- }
|
|
|
- }
|
|
|
+ self.manually_poll_events();
|
|
|
|
|
|
- pub fn current_lane(&mut self) -> &mut PriorityLane {
|
|
|
- match self.current_priority {
|
|
|
- EventPriority::Immediate => &mut self.lanes[0],
|
|
|
- EventPriority::High => &mut self.lanes[1],
|
|
|
- EventPriority::Medium => &mut self.lanes[2],
|
|
|
- EventPriority::Low => &mut self.lanes[3],
|
|
|
+ if !self.has_any_work() {
|
|
|
+ return committed_mutations;
|
|
|
}
|
|
|
- }
|
|
|
-
|
|
|
- pub fn handle_channel_msg(&mut self, msg: SchedulerMsg) {
|
|
|
- match msg {
|
|
|
- SchedulerMsg::Immediate(_) => todo!(),
|
|
|
- SchedulerMsg::UiEvent(_) => todo!(),
|
|
|
|
|
|
- //
|
|
|
- SchedulerMsg::SubmitTask(_, _) => todo!(),
|
|
|
- SchedulerMsg::ToggleTask(_) => todo!(),
|
|
|
- SchedulerMsg::PauseTask(_) => todo!(),
|
|
|
- SchedulerMsg::ResumeTask(_) => todo!(),
|
|
|
- SchedulerMsg::DropTask(_) => todo!(),
|
|
|
+ while self.has_any_work() {
|
|
|
+ self.prepare_work();
|
|
|
+ self.work_on_current_lane(|| false, &mut committed_mutations);
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- fn add_dirty_scope(&mut self, scope: ScopeId, priority: EventPriority) {
|
|
|
- todo!()
|
|
|
- // match priority {
|
|
|
- // EventPriority::High => self.high_priorty.dirty_scopes.insert(scope),
|
|
|
- // EventPriority::Medium => self.medium_priority.dirty_scopes.insert(scope),
|
|
|
- // EventPriority::Low => self.low_priority.dirty_scopes.insert(scope),
|
|
|
- // };
|
|
|
+ committed_mutations
|
|
|
}
|
|
|
|
|
|
+ /// Restart the entire VirtualDOM from scratch, wiping away any old state and components.
|
|
|
+ ///
|
|
|
+ /// Typically used to kickstart the VirtualDOM after initialization.
|
|
|
pub fn rebuild(&mut self, base_scope: ScopeId) -> Mutations {
|
|
|
- //
|
|
|
let mut shared = self.pool.clone();
|
|
|
let mut diff_machine = DiffMachine::new(Mutations::new(), &mut shared);
|
|
|
|
|
@@ -615,6 +594,8 @@ impl Scheduler {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+impl Scheduler {}
|
|
|
+
|
|
|
pub(crate) struct PriorityLane {
|
|
|
pub dirty_scopes: IndexSet<ScopeId>,
|
|
|
pub saved_state: Option<SavedDiffWork<'static>>,
|
|
@@ -631,7 +612,7 @@ impl PriorityLane {
|
|
|
}
|
|
|
|
|
|
fn has_work(&self) -> bool {
|
|
|
- todo!()
|
|
|
+ self.dirty_scopes.len() > 0 || self.in_progress == true
|
|
|
}
|
|
|
|
|
|
fn work(&mut self) {
|
|
@@ -824,3 +805,63 @@ impl ResourcePool {
|
|
|
|
|
|
pub fn borrow_bumpframe(&self) {}
|
|
|
}
|
|
|
+
|
|
|
+fn event_meta(event: &UserEvent) -> (bool, EventPriority) {
|
|
|
+ use EventPriority::*;
|
|
|
+
|
|
|
+ match event.name {
|
|
|
+ // clipboard
|
|
|
+ "copy" | "cut" | "paste" => (true, Medium),
|
|
|
+
|
|
|
+ // Composition
|
|
|
+ "compositionend" | "compositionstart" | "compositionupdate" => (true, Low),
|
|
|
+
|
|
|
+ // Keyboard
|
|
|
+ "keydown" | "keypress" | "keyup" => (true, High),
|
|
|
+
|
|
|
+ // Focus
|
|
|
+ "focus" | "blur" => (true, Low),
|
|
|
+
|
|
|
+ // Form
|
|
|
+ "change" | "input" | "invalid" | "reset" | "submit" => (true, Medium),
|
|
|
+
|
|
|
+ // Mouse
|
|
|
+ "click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit"
|
|
|
+ | "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter"
|
|
|
+ | "mouseleave" | "mouseout" | "mouseover" | "mouseup" => (true, High),
|
|
|
+
|
|
|
+ "mousemove" => (false, Medium),
|
|
|
+
|
|
|
+ // Pointer
|
|
|
+ "pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture"
|
|
|
+ | "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => {
|
|
|
+ (true, Medium)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Selection
|
|
|
+ "select" | "touchcancel" | "touchend" => (true, Medium),
|
|
|
+
|
|
|
+ // Touch
|
|
|
+ "touchmove" | "touchstart" => (true, Medium),
|
|
|
+
|
|
|
+ // Wheel
|
|
|
+ "scroll" | "wheel" => (false, Medium),
|
|
|
+
|
|
|
+ // Media
|
|
|
+ "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted"
|
|
|
+ | "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play"
|
|
|
+ | "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend"
|
|
|
+ | "timeupdate" | "volumechange" | "waiting" => (true, Medium),
|
|
|
+
|
|
|
+ // Animation
|
|
|
+ "animationstart" | "animationend" | "animationiteration" => (true, Medium),
|
|
|
+
|
|
|
+ // Transition
|
|
|
+ "transitionend" => (true, Medium),
|
|
|
+
|
|
|
+ // Toggle
|
|
|
+ "toggle" => (true, Medium),
|
|
|
+
|
|
|
+ _ => (true, Low),
|
|
|
+ }
|
|
|
+}
|