From f3538d302ace63053f628683cb915404366767b2 Mon Sep 17 00:00:00 2001 From: gozala Date: Thu, 26 May 2016 15:28:54 -0700 Subject: [PATCH] Combine Input, Assistant, Overlay & WebView into Navigator --- css/theme.css | 2 +- src/browser.js | 466 +++-------- src/browser/Navigators/Navigator.js | 868 ++++++++++++++++++++ src/browser/Navigators/Navigator/Display.js | 31 + src/browser/shell.js | 15 +- src/common/devtools.js | 1 + 6 files changed, 1005 insertions(+), 378 deletions(-) create mode 100644 src/browser/Navigators/Navigator.js create mode 100644 src/browser/Navigators/Navigator/Display.js diff --git a/css/theme.css b/css/theme.css index 954470963..35a123309 100644 --- a/css/theme.css +++ b/css/theme.css @@ -57,7 +57,7 @@ body.use-native-titlebar { background: rgba(0,0,0,0.1); } -.webview-is-dark .webview-combobox:hover { +.navigator.dark .webview-combobox:hover { background: rgba(255,255,255,0.1); } diff --git a/src/browser.js b/src/browser.js index 1a8ba1d5d..8266ac2fb 100644 --- a/src/browser.js +++ b/src/browser.js @@ -5,16 +5,13 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import {version} from "../package.json"; +import * as Package from "../package.json"; import * as Config from "../browserhtml.json"; import {Effects, html, forward, thunk} from "reflex"; import * as Shell from "./browser/shell"; -import * as Input from "./browser/input"; -import * as Assistant from "./browser/assistant"; -import * as Sidebar from './browser/sidebar'; import * as WebViews from "./browser/web-views"; -import * as Overlay from './browser/overlay'; +import * as Sidebar from './browser/Sidebar'; import * as Devtools from "./common/devtools"; import * as Runtime from "./common/runtime"; @@ -41,38 +38,24 @@ import type {Model, Action} from "./browser" export const init = ()/*:[Model, Effects]*/ => { const [devtools, devtoolsFx] = Devtools.init({isActive: Config.devtools}); - const [input, inputFx] = Input.init(false, false, ""); const [shell, shellFx] = Shell.init(); const [webViews, webViewsFx] = WebViews.init(); const [sidebar, sidebarFx] = Sidebar.init(); - const [assistant, assistantFx] = Assistant.init(); - const [overlay, overlayFx] = Overlay.init(false, false); const model = { version - , mode: 'create-web-view' , shell - , input - , assistant , webViews , sidebar - , overlay , devtools - , resizeAnimation: null - - , display: { rightOffset: 0 } - , isExpanded: true }; const fx = Effects.batch ( [ devtoolsFx.map(DevtoolsAction) - , inputFx.map(InputAction) , shellFx.map(ShellAction) , webViewsFx.map(WebViewsAction) , sidebarFx.map(SidebarAction) - , assistantFx.map(AssistantAction) - , overlayFx.map(OverlayAction) , Effects.receive(CreateWebView) , Effects .perform(Runtime.receive('mozbrowseropenwindow')) @@ -103,30 +86,7 @@ const SidebarAction = action => } ); -const OverlayAction = action => - ( action.type === "Click" - ? OverlayClicked - : { type: "Overlay" - , action - } - ); - -const InputAction = action => - ( action.type === 'Submit' - ? SubmitInput - : action.type === 'Abort' - ? ExitInput - : action.type === 'Blur' - ? BlurInput - : action.type === 'Query' - ? Query - : action.type === 'SuggestNext' - ? SuggestNext - : action.type === 'SuggestPrevious' - ? SuggestPrevious - : { type: 'Input' - , source: action } ); @@ -174,21 +134,6 @@ const DevtoolsAction = action => } ); -const AssistantAction = - action => - ( action.type === 'Suggest' - ? Suggest(action.source) - : { type: 'Assistant' - , source: action - } - ); - -const updateInput = cursor({ - get: model => model.input, - set: (model, input) => merge(model, {input}), - update: Input.update, - tag: InputAction -}); const updateWebViews = cursor({ get: model => model.webViews, @@ -211,13 +156,6 @@ const updateDevtools = cursor({ tag: DevtoolsAction }); -const updateAssistant = cursor({ - get: model => model.assistant, - set: (model, assistant) => merge(model, {assistant}), - update: Assistant.update, - tag: AssistantAction -}); - const updateSidebar = cursor({ get: model => model.sidebar, set: (model, sidebar) => merge(model, {sidebar}), @@ -225,13 +163,6 @@ const updateSidebar = cursor({ update: Sidebar.update }); -const updateOverlay = cursor({ - get: model => model.overlay, - set: (model, overlay) => merge(model, {overlay}), - tag: OverlayAction, - update: Overlay.update -}); - const Reloaded/*:Action*/ = { type: "Reloaded" }; @@ -282,19 +213,6 @@ export const DetachSidebar/*:Action*/ = , source: Sidebar.Detach }; -export const OverlayClicked/*:Action*/ = - { type: "OverlayClicked" - }; - -export const SubmitInput/*:Action*/ = - { type: 'SubmitInput' - }; - -export const ExitInput/*:Action*/ = - { type: 'ExitInput' - , source: Input.Abort - }; - export const Escape/*:Action*/ = { type: 'Escape' }; @@ -312,16 +230,6 @@ export const BlurInput/*:Action*/ = { type: 'BlurInput' }; -// ## Resize actions - -export const SuggestNext/*:Action*/ = { type: "SuggestNext" }; -export const SuggestPrevious/*:Action*/ = { type: "SuggestPrevious" }; -export const Suggest = tag('Suggest'); -export const Expand/*:Action*/ = {type: "Expand"}; -export const Expanded/*:Action*/ = {type: "Expanded"}; -export const Shrink/*:Action*/ = {type: "Shrink"}; -export const Shrinked/*:Action*/ = {type: "Shrinked"}; - // Following Browser actions directly delegate to a `WebViews` module, there for // they are just tagged versions of `WebViews` actions, but that is Just an @@ -350,7 +258,6 @@ const OpenURL = ({url}) => , uri: url } ); -const Query/*:Action*/ = { type: 'Query' }; export const ActivateWebViewByID = compose(WebViewsAction, WebViews.ActivateByID); @@ -368,22 +275,10 @@ const PrintSnapshot = { type: "PrintSnapshot" }; const PublishSnapshot = { type: "PublishSnapshot" }; export const Blur = ShellAction(Shell.Blur); export const Focus = ShellAction(Shell.Focus); +const ExpandSidebar = SidebarAction(Sidebar.Expand); +const CollapseSidebar = SidebarAction(Sidebar.Collapse); -const ShowInput = InputAction(Input.Show); -const HideInput = InputAction(Input.Hide); -const EnterInput = InputAction(Input.Enter); -const EnterInputSelection = compose(InputAction, Input.EnterSelection); -export const FocusInput = InputAction(Input.Focus); - -const OpenAssistant = AssistantAction(Assistant.Open); -const CloseAssistant = AssistantAction(Assistant.Close); -const ExpandAssistant = AssistantAction(Assistant.Expand); -const QueryAssistant = compose(AssistantAction, Assistant.Query); - -const OpenSidebar = SidebarAction(Sidebar.Open); -const CloseSidebar = SidebarAction(Sidebar.Close); - const DockSidebar = { type: "Sidebar" , action: Sidebar.Attach @@ -394,25 +289,10 @@ const UndockSidebar = , action: Sidebar.Detach }; -const HideOverlay = OverlayAction(Overlay.Hide); -const ShowOverlay = OverlayAction(Overlay.Show); -const FadeOverlay = OverlayAction(Overlay.Fade); - export const LiveReload = { type: 'LiveReload' }; -// Animation - -const ResizeAnimationAction = action => - ( { type: "ResizeAnimation" - , action - } - ); - - - - const modifier = OS.platform() == 'linux' ? 'alt' : 'accel'; const decodeKeyDown = Keyboard.bindings({ 'accel l': always(EditWebView), @@ -514,8 +394,6 @@ const selectWebView = (model, action) => ); -const submitInput = model => - update(model, NavigateTo(URL.read(model.input.value))); const openWebView = model => update @@ -600,209 +478,83 @@ const reloadRuntime = model => ]; -const updateQuery = - (model, action) => - updateAssistant - ( model - , Assistant.Query(model.input.value) - ); - -// Animations - -const expand = model => - ( model.isExpanded - ? [ model, Effects.none ] - : startResizeAnimation(merge(model, {isExpanded: true})) - ); - -const shrink = model => - ( model.isExpanded - ? startResizeAnimation(merge(model, {isExpanded: false})) - : [ model, Effects.none ] - ); - - -const startResizeAnimation = model => { - const [resizeAnimation, fx] = - Stopwatch.update(model.resizeAnimation, Stopwatch.Start); - return [ merge(model, {resizeAnimation}), fx.map(ResizeAnimationAction) ]; -} - -const endResizeAnimation = model => { - const [resizeAnimation, fx] = - Stopwatch.update(model.resizeAnimation, Stopwatch.End); - - return [ merge(model, {resizeAnimation}), Effects.none ]; -} - -const shrinked = endResizeAnimation; -const expanded = endResizeAnimation; - -const updateResizeAnimation = (model, action) => { - const [resizeAnimation, fx] = - Stopwatch.update(model.resizeAnimation, action); - const duration = 200; - - const [begin, end] = - ( model.isExpanded - ? [50, 0] - : [0, 50] - ); - - const result = - ( (resizeAnimation && duration > resizeAnimation.elapsed) - ? [ merge - ( model - , { resizeAnimation - , display: - merge - ( model.display - , { rightOffset - : Easing.ease - ( Easing.easeOutCubic - , Easing.float - , begin - , end - , duration - , resizeAnimation.elapsed - ) - } - ) - } - ) - , fx.map(ResizeAnimationAction) - ] - : [ merge - ( model - , { resizeAnimation - , display: merge(model.display, { rightOffset: end }) - } - ) - , Effects.receive - ( model.isExpanded - ? Expanded - : Shrinked - ) - ] - ); - - return result; -} - export const update = - (model/*:Model*/, action/*:Action*/)/*:[Model, Effects]*/ => - ( action.type === 'SubmitInput' - ? submitInput(model) - : action.type === 'OpenWebView' - ? openWebView(model) - : action.type === 'OpenURL' - ? openURL(model, action.uri) - : action.type === 'ReceiveOpenURLNotification' - ? reciveOpenURLNotification(model) - : action.type === 'ExitInput' - ? exitInput(model) - : action.type === 'CreateWebView' - ? createWebView(model) - : action.type === 'EditWebView' - ? editWebView(model) - : action.type === 'ShowWebView' - ? showWebView(model) - : action.type === 'ShowTabs' - ? showTabs(model) - : action.type === 'SelectWebView' - ? selectWebView(model) - // @TODO Change this to toggle tabs instead. - : action.type === 'Escape' - ? showTabs(model) - : action.type === 'AttachSidebar' - ? attachSidebar(model) - : action.type === 'DetachSidebar' - ? detachSidebar(model) - : action.type === 'ReloadRuntime' - ? reloadRuntime(model) - - // Expand / Shrink animations - : action.type === "Expand" - ? expand(model) - : action.type === "Shrink" - ? shrink(model) - : action.type === "ResizeAnimation" - ? updateResizeAnimation(model, action.action) - : action.type === "Expanded" - ? expanded(model) - : action.type === "Shrinked" - ? shrinked(model) - - // Delegate to the appropriate module - : action.type === 'Input' - ? updateInput(model, action.source) - : action.type === 'Suggest' - ? updateInput - ( model - , Input.Suggest - ( { query: model.assistant.query - , match: action.source.match - , hint: action.source.hint - } - ) - ) - - : action.type === 'BlurInput' - ? updateInput(model, Input.Blur) - - : action.type === 'WebViews' - ? updateWebViews(model, action.source) - : action.type === 'SelectTab' - ? updateWebViews(model, action.source) - : action.type === 'ActivateTabByID' - ? updateWebViews(model, action.activateTabByID) - : action.type === 'ActivateTab' - ? updateWebViews(model, action.activateTab) - - : action.type === 'Shell' - ? updateShell(model, action.source) - : action.type === 'Focus' - ? updateShell(model, Shell.Focus) - - // Assistant - : action.type === 'Assistant' - ? updateAssistant(model, action.source) - : action.type === 'Query' - ? updateQuery(model) - : action.type === 'SuggestNext' - ? updateAssistant(model, Assistant.SuggestNext) - : action.type === 'SuggestPrevious' - ? updateAssistant(model, Assistant.SuggestPrevious) - - : action.type === 'Devtools' - ? updateDevtools(model, action.action) - : action.type === 'Sidebar' - ? updateSidebar(model, action.action) - : action.type === 'Overlay' - ? updateOverlay(model, action.action) - - : action.type === 'Failure' - ? [ model - , Effects - .perform(Unknown.error(action.error)) - .map(NoOp) - ] - - // Ignore some actions. - : action.type === 'Reloaded' - ? [model, Effects.none] - : action.type === 'PrintSnapshot' - ? [model, Effects.none] - : action.type === 'UploadSnapshot' - ? [model, Effects.none] - // TODO: Delegate to modules that need to do cleanup. - : action.type === 'LiveReload' - ? [model, Effects.none] - - : Unknown.update(model, action) - ); + (model/*:Model*/, action/*:Action*/)/*:[Model, Effects]*/ => { + console.log(JSON.stringify(action)) + switch (action.type) { + case 'GoBack': + return goBack(model); + case 'GoForward': + return goForward(model); + case 'Reload': + return reload(model); + case 'ZoomIn': + return zoomIn(model); + case 'ZoomOut': + return zoomOut(model); + case 'ResetZoom': + return resetZoom(model); + case 'Close': + return close(model); + case 'OpenNewTab': + return openNewTab(model); + case 'EditWebView': + return editWebView(model); + case 'ShowWebView': + return showWebView(model); + case 'ShowTabs': + return showTabs(model); + case 'Escape': + return toggleTabs(model); + case 'AttachSidebar': + return attachSidebar(model); + case 'DetachSidebar': + return detachSidebar(model); + case 'ReloadRuntime': + return reloadRuntime(model); + case 'SelectNext': + return selectNext(model); + case 'SelectPrevious': + return selectPrevious(model); + case 'EndSelection': + return endSelection(model); + case 'Shell': + return updateShell(model, action.source); + case 'Focus': + return updateShell(model, Shell.Focus); + case 'Devtools': + return updateDevtools(model, action.action); + case 'Sidebar': + return updateSidebar(model, action.action); + case 'Tabs': + return updateNavigators(model, action); + case 'Navigators': + return updateNavigators(model, action.navigators); + case 'Failure': + return [ + model + , Effects + .perform(Unknown.error(action.error)) + .map(NoOp) + ]; + + // Ignore some actions. + case 'Reloaded': + return [ model, Effects.none ] + case 'PrintSnapshot': + return [model, Effects.none]; + case 'UploadSnapshot': + return [model, Effects.none]; + // TODO: Delegate to modules that need to do cleanup. + case 'LiveReload': + return [model, Effects.none]; + + default: + return Unknown.update(model, action); + } + }; const styleSheet = StyleSheet.create({ root: { @@ -837,59 +589,25 @@ export const view = , onFocus: onWindow(address, always(Focus)) , onUnload: onWindow(address, always(Unload)) } - , [ html.div - ( { className: 'browser-content' - , style: - Style - ( styleSheet.content - , { width: `calc(100vw - ${model.display.rightOffset}px)` - } - ) - } - , [ thunk - ( 'web-views' - , WebViews.view - , model.webViews - , forward(address, WebViewsAction) - ) - , thunk - ( 'overlay' - , Overlay.view - , model.overlay - , forward(address, OverlayAction)) - , thunk - ( 'assistant' - , Assistant.view - , model.assistant - , forward(address, AssistantAction) - ) - , thunk - ( 'input' - , Input.view - , model.input - , forward(address, InputAction) - ) - , thunk - ( 'devtools' - , Devtools.view - , model.devtools - , forward(address, DevtoolsAction) - ) - ] + , [ Webviews.view + ( model.navigators + , forward(address, NavigatorsAction) ) - , thunk - ( 'sidebar' - , Sidebar.view - , model.sidebar - , model.webViews + + , Sidebar.view + ( model.sidebar + , model.navigators.deck , forward(address, SidebarAction) ) - , thunk - ( 'shell' - , Shell.view - , model.shell + , Shell.view + ( model.shell , forward(address, ShellAction) ) + + , Devtools.view + ( model.devtools + , forward(address, DevtoolsAction) + ) ] ); diff --git a/src/browser/Navigators/Navigator.js b/src/browser/Navigators/Navigator.js new file mode 100644 index 000000000..5a9b74ca2 --- /dev/null +++ b/src/browser/Navigators/Navigator.js @@ -0,0 +1,868 @@ +/* @flow */ + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import {Effects, html, forward, thunk} from "reflex" +import {merge, always, batch} from "../../common/prelude"; +import {cursor} from "../../common/cursor"; +import * as Style from "../../common/style"; + +import * as Assistant from "./Navigator/Assistant"; +import * as Overlay from "./Navigator/Overlay"; +import * as Input from "./Navigator/Input"; +import * as Output from "./Navigator/WebView"; +import * as Unknown from "../../common/unknown"; +import * as URL from '../../common/url-helper'; +import * as Header from './Navigator/Header'; +import * as Progress from './Navigator/Progress'; +import * as Display from './Navigator/Display'; +import * as Animation from "../../common/Animation"; +import * as Easing from "eased"; +import * as Tab from "../Sidebar/Tab"; + +import {readTitle, isSecure, isDark, canGoBack} from './Navigator/WebView/Util'; + +/*:: +import type {Address, DOM} from "reflex" +import type {URI, Time} from "./Navigator/WebView" + +export type Flags = + { output: Output.Flags + , input?: Input.Flags + , overlay?: Overlay.Flags + , assistant?: Assistant.Flags + } + +export type Action = + | { type: "NoOp" } + + // Card + | { type: "Deselect" } + | { type: "Select" } + | { type: "Close" } + | { type: "Closed" } + + + // Input + | { type: "CommitInput" } + | { type: "SubmitInput" } + | { type: "EscapeInput" } + | { type: "FocusInput" } + | { type: "AbortInput" } + | { type: "SuggestNext" } + | { type: "SuggestPrevious" } + | { type: "Input", input: Input.Action } + + // Output + | { type: "GoBack" } + | { type: "GoForward" } + | { type: "Reload" } + | { type: "ZoomIn" } + | { type: "ZoomOut" } + | { type: "ResetZoom" } + | { type: "FocusOutput" } + // | { type: "PushedDown" } + | { type: "LoadStart", time: Time } + | { type: "Connect", time: Time } + | { type: "LoadEnd", time: Time } + + | { type: "Output", output: Output.Action } + + // Assistant + | { type: "Suggest", suggest: Assistant.Suggestion } + | { type: "Assistant", assistant: Assistant.Action } + + // Overlay + | { type: "Overlay", overlay: Overlay.Action } + | { type: "HideOverlay" } + | { type: "ShowOverlay" } + + // Progress + | { type: "Progress", progress: Progress.Action } + + // Header + | { type: "ShowTabs" } + | { type: "OpenNewTab" } + | { type: "EditInput" } + | { type: "Header", header: Header.Action } + + // Internal + | { type: "ActivateAssistant"} + | { type: "DeactivateAssistant" } + | { type: "SetSelectedInputValue", value: string } + + // Embedder + | { type: "Navigate", uri: URI } + | { type: "Open" + , open: + { output: Output.Flags + , input?: Input.Flags + , assistant?: Assistant.Flags + , overlay?: Overlay.Flags + } + } + + | { type: "Tab", tab: Tab.Action } + + // Animation + | { type: "Animation", animation: Animation.Action } + | { type: "AnimationEnd" } +*/ + +const SubmitInput = { type: "SubmitInput" } +const EscapeInput = { type: "EscapeInput" } +const FocusInput = { type: "FocusInput" } +const CommitInput = { type: "CommitInput" } +const SuggestNext = { type: "SuggestNext" } +const SuggestPrevious = { type: "SuggestPrevious" } +export const GoBack = { type: "GoBack" } +export const GoForward = { type: "GoForward" } +export const Reload = { type: "Reload" } +export const ZoomOut = { type: "ZoomOut" } +export const ZoomIn = { type: "ZoomIn" } +export const ResetZoom = { type: "ResetZoom" } + +const ShowTabs = { type: "ShowTabs" }; +const OpenNewTab = { type: "OpenNewTab"}; +export const EditInput = { type: "EditInput" }; +const FocusOutput = { type: "FocusOutput" }; +const AbortInput = { type: "AbortInput" }; +const HideOverlay = { type: "HideOverlay" }; +const ShowOverlay = { type: "ShowOverlay" }; + +export const Close = { type: "Close" }; +export const Closed = { type: "Closed" }; +export const Deactivate = { type: "Deactivate" } +export const Activate = { type: "Activate" } +export const Deselect = { type: "Deselect" } +export const Select = { type: "Select" } + +const tagInput = + action => { + switch (action.type) { + case "Submit": + return SubmitInput + case "Abort": + return EscapeInput + case "Focus": + return FocusInput + case "Query": + return CommitInput + case "SuggestNext": + return SuggestNext + case "SuggestPrevious": + return SuggestPrevious + default: + return { type: 'Input', input: action } + } + } + +const tagAssistant = + action => { + switch (action.type) { + case "Suggest": + return { type: "Suggest", suggest: action.suggest } + default: + return { type: "Assistant", assistant: action } + } + } + +const tagOverlay = + action => { + switch (action.type) { + case "Click": + return EscapeInput + default: + return { type: "Overlay", overlay: action } + } + } + + +const tagOutput = + action => { + switch (action.type) { + case "Create": + return OpenNewTab + case "Focus": + return FocusOutput + case "Close": + return Close; + case "Open": + return { + type: "Open" + , open: + { output: action.options + } + } + case "LoadStart": + return action + case "Connect": + return action + case "LoadEnd": + return action + default: + return { type: "Output", output: action } + } + }; + +const tagHeader = + action => { + switch (action.type) { + case "EditInput": + return EditInput + case "ShowTabs": + return ShowTabs + case "OpenNewTab": + return OpenNewTab + case "GoBack": + return GoBack + default: + return { type: "Header", header: action } + } + } + +const tagProgress = + action => { + switch (action.type) { + default: + return { type: "Progress", progress: action } + } + } + +const tagAnimation = + action => { + switch (action.type) { + case "End": + return { type: "AnimationEnd" }; + default: + return { type: "Animation", animation: action } + } + }; + +export const Navigate = + ( destination/*:string*/)/*:Action*/ => + ( { type: "Navigate" + , uri: URL.read(destination) + } + ) + +const ActivateAssistant = { type: "ActivateAssistant" } +const DeactivateAssistant = { type: "DeactivateAssistant" } + +const SetSelectedInputValue = + value => + ( { type: "SetSelectedInputValue" + , value + } + ) + +export class Model { + /*:: + isSelected: boolean; + isClosed: boolean; + output: Output.Model; + input: Input.Model; + overlay: Overlay.Model; + assistant: Assistant.Model; + progress: Progress.Model; + animation: Animation.Model; + */ + constructor( + isSelected/*:boolean*/ + , isClosed/*:boolean*/ + , input/*:Input.Model*/ + , output/*:Output.Model*/ + , assistant/*:Assistant.Model*/ + , overlay/*:Overlay.Model*/ + , progress/*:Progress.Model*/ + , animation/*:Animation.Model*/ + ) { + this.isSelected = isSelected + this.isClosed = isClosed + this.input = input + this.output = output + this.assistant = assistant + this.overlay = overlay + this.progress = progress + this.animation = animation + } +} + +const assemble = + ( isSelected + , isClosed + , [input, $input] + , [output, $output] + , [assistant, $assistant] + , [overlay, $overlay] + , [progress, $progress] + , [animation, $animation] + ) => { + const model = new Model + ( isSelected + , isClosed + , input + , output + , assistant + , overlay + , progress + , animation + ) + + const fx = Effects.batch + ( [ $input.map(tagInput) + , $output.map(tagOutput) + , $overlay.map(tagOverlay) + , $assistant.map(tagAssistant) + , $progress.map(tagProgress) + , $animation.map(tagAnimation) + ] + ) + + return [model, fx] + } + +export const init = + (options/*:Flags*/)/*:[Model, Effects]*/ => + assemble + ( options.output.disposition != 'background-tab' + , false + , Input.init(options.input) + , Output.init(options.output) + , Assistant.init(options.assistant) + , Overlay.init(options.overlay) + , Progress.init() + , Animation.init + ( options.output.disposition != 'background-tab' + ? Display.selected + : Display.deselected + ) + ) + +export const update = + ( model/*:Model*/ + , action/*:Action*/ + )/*:[Model, Effects]*/ => { + // console.log(action) + switch (action.type) { + case 'NoOp': + return nofx(model); + + case 'Navigate': + return navigate(model, action.uri); + + case 'Select': + return select(model); + case 'Deselect': + return deselect(model); + case 'Close': + return close(model); + + // Input + case 'CommitInput': + return commitInput(model); + case 'SubmitInput': + return submitInput(model); + case 'EscapeInput': + return escapeInput(model); + case 'FocusInput': + return focusInput(model); + case 'AbortInput': + return abortInput(model); + case 'SuggestNext': + return suggestNext(model); + case 'SuggestPrevious': + return suggestPrevious(model); + case 'Input': + return updateInput(model, action.input); + case 'Tab': + return updateOutput(model, action); + + // Output + case "GoBack": + return updateOutput(model, Output.GoBack); + case "GoForward": + return updateOutput(model, Output.GoForward); + case "Reload": + return updateOutput(model, Output.Reload); + case "ZoomIn": + return updateOutput(model, Output.ZoomIn); + case "ZoomOut": + return updateOutput(model, Output.ZoomOut); + case "ResetZoom": + return updateOutput(model, Output.ResetZoom); + + case 'FocusOutput': + return focusOutput(model); + case 'EditInput': + return editInput(model); + case "LoadStart": + return updateLoadProgress(model, action); + case "Connect": + return updateLoadProgress(model, action); + case "LoadEnd": + return updateLoadProgress(model, action); + case 'Output': + return updateOutput(model, action.output); + + // Progress + case 'Progress': + return updateProgress(model, action.progress); + + // Assistant + case 'Suggest': + return suggest(model, action.suggest); + case 'Assistant': + return updateAssistant(model, action.assistant); + + case 'Overlay': + return updateOverlay(model, action.overlay); + case 'HideOverlay': + return updateOverlay(model, Overlay.Hide); + case 'ShowOverlay': + return updateOverlay(model, Overlay.Show); + + // Internal + case 'ActivateAssistant': + return activateAssistant(model); + case 'DeactivateAssistant': + return deactivateAssistant(model); + case 'SetSelectedInputValue': + return setSelectedInputValue(model, action.value); + + case 'Animation': + return updateAnimation(model, action.animation); + case 'AnimationEnd': + return endAnimation(model); + + default: + return Unknown.update(model, action); + } + }; + +const nofx = + (model/*:Model*/)/*:[Model, Effects]*/ => + [ model + , Effects.none + ]; + +export const select = + ( model/*:Model*/ + )/*:[Model, Effects]*/ => + ( model.isSelected + ? nofx(model) + : startAnimation + ( model + , true + , model.isClosed + , Animation.transition + ( model.animation + , Display.selected + , 80 + ) + ) + ) + +export const deselect = + ( model/*:Model*/ + )/*:[Model, Effects]*/ => + ( model.isSelected + ? startAnimation + ( model + , false + , model.isClosed + , Animation.transition + ( model.animation + , Display.deselected + , 80 + ) + ) + : nofx(model) + ) + +export const close = + ( model/*:Model*/ + )/*:[Model, Effects]*/ => + ( model.isSelected + ? startAnimation + ( model + , false + , true + , Animation.transition + ( model.animation + , Display.closed + , 80 + ) + ) + : [ model + , Effects.receive(Closed) + ] + ) + +const navigate = + (model, uri) => + updateOutput + ( model + , Output.Load(uri) + ) + +const commitInput = + model => + updateAssistant + ( model + , Assistant.Query(model.input.value) + ) + +const submitInput = + model => + batch + ( update + , model + , [ FocusOutput + , Navigate(model.input.value) + ] + ); + +const escapeInput = + model => + batch + ( update + , model + , [ DeactivateAssistant + , AbortInput + , FocusOutput + , HideOverlay + ] + ); + +const focusInput = + model => + updateInput(model, Input.Focus); + +const abortInput = + model => + updateInput(model, Input.Abort); + + +const suggestNext = + model => + updateAssistant(model, Assistant.SuggestNext); + +const suggestPrevious = + model => + updateAssistant(model, Assistant.SuggestPrevious); + +const focusOutput = + model => + updateOutput(model, Output.Focus); + +const goBack = + model => + updateOutput(model, Output.GoBack); + +const updateLoadProgress = + (source, action) => { + const [output, output$] = Output.update(source.output, action) + const [progress, progress$] = Progress.update(source.progress, action) + const model = new Model + ( source.isSelected + , source.isClosed + , source.input + , output + , source.assistant + , source.overlay + , progress + , source.animation + ) + const fx = Effects.batch + ( [ output$.map(tagOutput) + , progress$.map(tagProgress) + ] + ) + + return [model, fx] + } + + +const editInput = + model => + batch + ( update + , model + , [ FocusInput + , ActivateAssistant + , ShowOverlay + // @TODO: Do not use `model.output.navigation.currentURI` as it ties it + // to webView API too much. + , SetSelectedInputValue(model.output.navigation.currentURI) + ] + ) + +const suggest = + (model, suggestion) => + updateInput + ( model + , Input.Suggest + ( { query: model.assistant.query + , match: suggestion.match + , hint: suggestion.hint + } + ) + ) + +const activateAssistant = + model => + updateAssistant + ( model + , Assistant.Open + ) + +const deactivateAssistant = + model => + updateAssistant + ( model + , Assistant.Close + ) + +const setSelectedInputValue = + (model, value) => + updateInput + ( model + , Input.EnterSelection(value) + ) + +const updateInput = cursor + ( { get: model => model.input + , set: + (model, input) => + new Model + ( model.isSelected + , model.isClosed + , input + , model.output + , model.assistant + , model.overlay + , model.progress + , model.animation + ) + , update: Input.update + , tag: tagInput + } + ); + +const updateOutput = cursor + ( { get: model => model.output + , set: + (model, output) => + new Model + ( model.isSelected + , model.isClosed + , model.input + , output + , model.assistant + , model.overlay + , model.progress + , model.animation + ) + , update: Output.update + , tag: tagOutput + } + ); + +const updateProgress = cursor + ( { get: model => model.progress + , set: + (model, progress) => + new Model + ( model.isSelected + , model.isClosed + , model.input + , model.output + , model.assistant + , model.overlay + , progress + , model.animation + ) + , update: Progress.update + , tag: tagProgress + } + ); + +const updateAssistant = cursor + ( { get: model => model.assistant + , set: + (model, assistant) => + new Model + ( model.isSelected + , model.isClosed + , model.input + , model.output + , assistant + , model.overlay + , model.progress + , model.animation + ) + , update: Assistant.update + , tag: tagAssistant + } + ); + +const updateOverlay = cursor + ( { get: model => model.overlay + , set: + (model, overlay) => + new Model + ( model.isSelected + , model.isClosed + , model.input + , model.output + , model.assistant + , overlay + , model.progress + , model.animation + ) + , update: Overlay.update + , tag: tagOverlay + } + ); + +const animate = + (animation, action) => + Animation.updateWith + ( Easing.easeOutCubic + , Display.interpolate + , animation + , action + ) + +const updateAnimation = cursor + ( { get: model => model.animation + , set: + (model, animation) => + new Model + ( model.isSelected + , model.isClosed + , model.input + , model.output + , model.assistant + , model.overlay + , model.progress + , animation + ) + , tag: tagAnimation + , update: animate + } + ) + +const startAnimation = + (model, isSelected, isClosed, [animation, fx]) => + [ new Model + ( isSelected + , isClosed + , model.input + , model.output + , model.assistant + , model.overlay + , model.progress + , animation + ) + , fx.map(tagAnimation) + ] + +const endAnimation = + model => + ( model.isClosed + ? [ model + , Effects.receive(Closed) + ] + : nofx(model) + ) + +export const render = + (model/*:Model*/, address/*:Address*/)/*:DOM*/ => + html.dialog + ( { className: `navigator ${mode(model.output)}` + , open: true + , style: Style.mix + ( styleSheet.base + , ( isDark(model.output) + ? styleSheet.dark + : styleSheet.bright + ) + , ( model.isSelected + ? styleSheet.selected + : styleSheet.unselected + ) + , model.animation.state + , styleBackground(model.output) + ) + } + , [ Header.view + ( readTitle(model.output, 'Untitled') + , isSecure(model.output) + , canGoBack(model.output) + , forward(address, tagHeader) + ) + , Progress.view(model.progress, forward(address, tagProgress)) + , Input.view(model.input, forward(address, tagInput)) + , Assistant.view(model.assistant, forward(address, tagAssistant)) + , Output.view(model.output, forward(address, tagOutput)) + , Overlay.view(model.overlay, forward(address, tagOverlay)) + ] + ) + +export const view = + (model/*:Model*/, address/*:Address*/)/*:DOM*/ => + thunk + ( model.output.ref.value + , render + , model + , address + ) + +const styleSheet = Style.createSheet + ( { base: + { width: '100%' + , height: '100%' + , position: 'absolute' + , top: 0 + , left: 0 + , overflow: 'hidden' + , backgroundColor: 'white' + , display: 'block' + , borderRadius: '4px' + , transitionProperty: 'background-color, color, border-color' + , transitionTimingFunction: 'ease-in, ease-out, ease' + , transitionDuration: '300ms' + } + , selected: + { zIndex: 2 } + , unselected: + { zIndex: 1 } + , dark: + { color: 'rgba(255, 255, 255, 0.8)' + , borderColor: 'rgba(255, 255, 255, 0.2)' + } + , bright: + { color: 'rgba(0, 0, 0, 0.8)' + , borderColor: 'rgba(0, 0, 0, 0.2)' + } + } + ); + +const styleBackground = + model => + ( model.page.pallet.background + ? { backgroundColor: model.page.pallet.background + } + : null + ) + +const mode = + model => + ( isDark(model) + ? 'dark' + : 'bright' + ) diff --git a/src/browser/Navigators/Navigator/Display.js b/src/browser/Navigators/Navigator/Display.js new file mode 100644 index 000000000..3c99c85aa --- /dev/null +++ b/src/browser/Navigators/Navigator/Display.js @@ -0,0 +1,31 @@ +/* @flow */ + +import * as Easing from "eased"; + +/*:: +import type {Float} from "../../../common/prelude" +*/ + +export class Model { + /*:: + opacity: Float; + */ + constructor( + opacity/*:Float*/ + ) { + this.opacity = opacity + } +} + +export const selected = new Model(1) +export const deselected = new Model(0) +export const closed = new Model(0) + +export const interpolate = + ( from/*:Model*/ + , to/*:Model*/ + , progress/*:Float*/ + )/*:Model*/ => + new Model + ( Easing.float(from.opacity, to.opacity, progress) + ) diff --git a/src/browser/shell.js b/src/browser/shell.js index bddaa63f8..cd5681438 100644 --- a/src/browser/shell.js +++ b/src/browser/shell.js @@ -4,7 +4,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import {Effects, Task, html, forward} from "reflex"; +import {Effects, Task, html, thunk, forward} from "reflex"; import {merge, always} from "../common/prelude"; import {cursor} from "../common/cursor"; import * as Focusable from "../common/focusable"; @@ -187,11 +187,20 @@ export const update = : Unknown.update(model, action) ); -export const view = +export const render = (model/*:Model*/, address/*:Address*/)/*:DOM*/ => { if (!Runtime.useNativeTitlebar()) { return Controls.view(model.controls, forward(address, ControlsAction)); } else { - return html.div(); + return ""; } } + +export const view = + (model/*:Model*/, address/*:Address*/)/*:DOM*/ => + thunk + ( "Browser/Shell" + , render + , model + , address + ); diff --git a/src/common/devtools.js b/src/common/devtools.js index 557c442aa..355bbcb61 100644 --- a/src/common/devtools.js +++ b/src/common/devtools.js @@ -219,6 +219,7 @@ const styleSheet = StyleSheet.create , backgroundColor: 'white' , border: '2px solid #F06' , overflow: 'scroll' + , zIndex: 3 } , initializing: { display: 'none'